mq日志怎么看_SpringBoot集成websocket发送后台日志到前台页面

业务需求

后台为一个采集系统,需要将采集过程中产生的日志实时发送到前台页面展示,以便了解采集过程。

技能点

  • SpringBoot 2.x

  • websocket

  • logback

  • thymeleaf

  • RabbitMQ

之所以使用到RabbitMQ是因为实际环境中采集服务为多个,为了统一处理日志信息,将日志都先灌入mq中,再统一从mq中进行消费

引入关键pom

<dependency>  <groupId>org.springframework.bootgroupId>  <artifactId>spring-boot-starter-webartifactId>dependency><dependency>  <groupId>org.springframework.bootgroupId>  <artifactId>spring-boot-starter-websocketartifactId>dependency><dependency>  <groupId>org.springframework.bootgroupId>  <artifactId>spring-boot-starter-amqpartifactId>dependency>

logback配置文件引入AmqpAppender

<springProperty scope="context" name="rabbitmq-address" source="spring.rabbitmq.addresses" defaultValue="127.0.0.1:5672" /><springProperty scope="context" name="rabbitmq-username" source="spring.rabbitmq.username" defaultValue="guest" /><springProperty scope="context" name="rabbitmq-password" source="spring.rabbitmq.password" defaultValue="guest" /><springProperty scope="context" name="rabbitmq-virtual-host" source="spring.rabbitmq.virtual-host" defaultValue="/" /><springProperty scope="context" name="exhcange-name" source="platform.parameter.exhcangeName" defaultValue="default-exchange" /><springProperty scope="context" name="binding-key" source="platform.parameter.bindingKey" defaultValue="default-routing" /><appender name="RabbitMq"  class="org.springframework.amqp.rabbit.logback.AmqpAppender">  <layout>    <pattern>[%X{traceId}] - %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%npattern>       layout>    <addresses>${rabbitmq-address}addresses>  <username>${rabbitmq-username}username>  <password>${rabbitmq-password}password>  <virtualHost>${rabbitmq-username}virtualHost>    <declareExchange>falsedeclareExchange>  <exchangeType>directexchangeType>  <exchangeName>${exhcange-name}exchangeName>  <routingKeyPattern>${binding-key}routingKeyPattern>  <generateId>truegenerateId>  <charset>UTF-8charset>  <durable>truedurable>  <deliveryMode>NON_PERSISTENTdeliveryMode>  <filter class="com.log.websocket.stomp.LogFilter">      <level>INFOlevel>  filter>appender><springProfile name="dev">  <root level="debug">    <appender-ref ref="RabbitMq" />  root>springProfile>

日志过滤器

logback配置文件中添加的AmqpAppender使用了filter,具体的filter如下所示:

public class LogFilter extends AbstractMatcherFilter<ILoggingEvent> {  Level level;  @Override  public FilterReply decide(ILoggingEvent event) {    if (!isStarted()) {      return FilterReply.NEUTRAL;    }    //过滤指定级别的日志    if(event.getLevel().equals(level)){      Map mdcMap = event.getMDCPropertyMap();      String tracId = mdcMap.get("traceId");      //过滤日志中带有traceId的日志,其他的不需要,traceId使用aop添加      if(StringUtils.isNotBlank(tracId)){        return FilterReply.ACCEPT;      }    }    return FilterReply.DENY;  }  public void setLevel(Level level) {    this.level = level;  }  @Override  public void start() {    if (this.level != null) {      super.start();    }  }}

说明:

AmqpAppender中的filter设置了过滤级别,因此只过滤指定级别的日志;

过滤日志中带有traceId的日志,traceId通过aop添加,具体参考后面的aop设置;

aop方式添加traceId

编写LogAspect如下所示:

@Order(1)@Aspect@Componentpublic class LogAspect {    /**     * 所有的业务类的类名都是xxSpiderxxImpl,统一入口都是gatherData方法     */    @Pointcut("execution(* com.log..*.service..*Spider*Impl.gatherData(..))")    public void pointCut() {}    @Before("pointCut()")    public void before(JoinPoint joinPoint){        //切点已经确定是com.log..*.service..*Spider*Impl.gatherData(..),该方法的参数只有一个,且为GatherTaskVO        GatherTaskVO vo = (GatherTaskVO)joinPoint.getArgs()[0];        //将任务id作为traceId        MDC.put("traceId", vo.getId());    }    @After("pointCut()")    public void after(JoinPoint joinPoint){        //方法执行完成以后,删除traceId        MDC.remove("traceId");    }}

解释一下MDC:

对于多个线程同时执行的系统或者分布式系统中,各个线程的日志穿插执行,导致我们无法直观的直接定位整个操作流程,因此,我们需要对一个线程的操作流程进行归类标记,比如使用线程+时间戳或者用户id等,从而使我们能够从混乱的日志中梳理处整个线程的操作流程,因此Slf4j的MDC应运而生,logback和log4j支持MDC。

MDC中提供的方法如下所示:

package org.jboss.logging;import java.util.Collections;import java.util.Map;/** * Mapped diagnostic context. Each log provider implementation may behave different. */public final class MDC {   //uts the value onto the context.    public static Object put(String key, Object val);    //Returns the value for the key or {@code null} if no value was found.    public static Object get(String key);  //Removes the value from the context.    public static void remove(String key);   //Clears the message diagnostics context.    public static void clear();}

MDC提供的方法比较简单,使用也很简单,只需要将指定的值put到线程上下文中,在对应的地方调用get方法获取到值即可。

注意看上述AmqpAppender配置中标记<1>中的traceId即为我们此处添加到线程上下文中的值,如下所示:

  [%X{traceId}] - %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern></layout>

开启websocket支持

Springboot环境下注入ServerEndpointExporter以开启websocket支持

@Configurationpublic class WebSocketConfig {    @Bean    public ServerEndpointExporter serverEndpointExporter() {        return new ServerEndpointExporter();    }}

websocketServer

websocketServer用来开启连接,关闭连接以及接收消息等

@Slf4j@ServerEndpoint("/socketserver/{taskId}")@Componentpublic class WebSocketServer {    /**concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。*/    private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<>();    /**与某个客户端的连接会话,需要通过它来给客户端发送数据*/    private Session session;    /**接收taskId*/    private String taskId="";    /**     * 连接建立成功调用的方法*/    @OnOpen    public void onOpen(Session session,@PathParam("taskId") String taskId) {        this.session = session;        this.taskId=taskId;        if(webSocketMap.containsKey(taskId)){            webSocketMap.remove(taskId);            webSocketMap.put(taskId,this);        }else{            webSocketMap.put(taskId,this);        }        try {            sendMessage("socket连接成功");        } catch (IOException e) {            log.error("socket>>"+taskId+",网络异常!!!!!!");        }    }    /**     * 连接关闭调用的方法     */    @OnClose    public void onClose() {        if(webSocketMap.containsKey(taskId)){            webSocketMap.remove(taskId);        }    }    /**     * 收到客户端消息后调用的方法     * TODO 客户端交互使用,暂无用到     * @param message 客户端发送过来的消息*/    @OnMessage    public void onMessage(String message, Session session) {        log.info("socket>>>:"+taskId+",报文:"+message);    }    /**     *     * @param session     * @param error     */    @OnError    public void onError(Session session, Throwable error) {        log.error("用户错误:"+this.taskId+",原因:"+error.getMessage());        error.printStackTrace();    }    /**     * 实现服务器主动推送     */    public void sendMessage(String message) throws IOException {        //加锁,否则会出现java.lang.IllegalStateException: The remote endpoint was in state [TEXT_FULL_WRITING] which is an invalid state for called method异常,并发使用session发送消息导致的        synchronized (this.session){            this.session.getBasicRemote().sendText(message);        }    }    public ConcurrentHashMap<String,WebSocketServer> getWebSocketMap(){ return webSocketMap; }}

前台页面

前台页面使用js来调用websocket,请求websocketserver打开socket连接,并且开始和后台交互发送消息

<html xmlns:th="http://www.thymeleaf.org" ><head>    <meta charset="utf-8">    <title>任务日志展示title>head><body><script th:src="@{/js/jquery.min.js}">script><input type="hidden" id="gather_task_id" th:value="${taskId}" /><script>    var socket;    function openSocket() {        var detailDiv = $("#log_detail");        var taskId = $("#gather_task_id").val();        //实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接        var socketUrl="http://localhost:8888/socketserver/"+taskId;        socketUrl=socketUrl.replace("https","ws").replace("http","ws");        if(socket!=null){            socket.close();            socket=null;        }        socket = new WebSocket(socketUrl);        //打开事件        socket.onopen = function() {            console.log("websocket已打开");        };        //获得消息事件        socket.onmessage = function(msg) {            console.log(msg.data);            //发现消息进入    开始处理前端触发逻辑            detailDiv.append("

"

+msg.data+"") }; //关闭事件 socket.onclose = function() { console.log("websocket已关闭"); }; //发生了错误事件 socket.onerror = function() { console.log("websocket发生了错误"); } } function sendMessage() { if(typeof(WebSocket) == "undefined") { console.log("您的浏览器不支持WebSocket"); }else { console.log("您的浏览器支持WebSocket"); console.log('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'"}'); socket.send('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'"}'); } } function printLog(){ if(typeof(WebSocket) == "undefined") { console.log("您的浏览器不支持WebSocket"); alert("您的浏览器不支持WebSocket"); }else { openSocket(); } } function quit(){ if(socket!=null){ socket.close(); socket=null; var detailDiv = $("#log_detail"); detailDiv.append("

客户端已退出

") } }script><a href="javascript:void(0);" onclick="printLog()" >打印日志a><a href="javascript:void(0);" onclick="quit()">退出a><div id="log_detail">div>body>html>

消费mq中的日志消息

service中产生的日志是添加到mq队列中的,因此需要一个消费者消费队列中的数据,并且使用websocketserver将消息发送到对应的页面上,从而在页面上进行展示

@Component@Slf4jpublic class LogConsumer {    @Resource    private WebSocketService webSocketService;    @RabbitHandler    @RabbitListener(            bindings = @QueueBinding(                    value = @Queue(name = "${platform.parameter.queueName}",durable = "true"),                    exchange = @Exchange(name = "${platform.parameter.exhcangeName}",ignoreDeclarationExceptions="true",durable = "true"),                    key = "${platform.parameter.bindingKey}"            ),            concurrency = "2"    )    public void listennerPush(String msg, Channel channel, Message message) throws IOException {        try {            log.debug("consumer>>>接收到的消息>>>{}",msg);            //[1] - 13:15:17.484 - TwitterSpiderMobileService实现类方法<<<            msg.split(" - ")[0].trim().replace("[","").replace("]","");            String tracId =  msg.substring(0,msg.indexOf(" - ")).trim().replace("[","").replace("]","");            msg = msg.substring(msg.indexOf(" - ")+2);            //调用websocket发送日志信息到页面上            webSocketService.sendMessage(tracId,msg);        } catch (Exception e) {            log.error("获取消息失败,异常原因:{}",e.getMessage(),e);        } finally {            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);        }    }}

sendMessage方法如下所示:

@Overridepublic void sendMessage(String taskId, String logMessage) {  try {    ConcurrentHashMapmap =  webSocketServer.getWebSocketMap();    WebSocketServer server =  map.get(taskId);    if(server!=null){      server.sendMessage(logMessage);    }else{      log.warn("客户端已退出");    }  } catch (IOException e) {    log.error("向客户端发送消息时出现异常,异常原因:{}",e.getMessage(),e);  }}

最终效果图

经过以上步骤即可将service中生成的日志接近实时的显示在前台页面上,最后的显示效果如下所示:

97f1b84df842c2a2a035e311a51e0255.png

参考资料

1.SpringBoot2.0集成WebSocket,实现后台向前端推送信息(https://blog.csdn.net/moshowgame/article/details/80275084)

本文所对应的代码已上传gitee(https://gitee.com/liaidai/log-websocket),有需要的可以自行下载。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值