Spring boot + websocket实现实时日志

1,027 阅读2分钟

Demo(只包含方案1)

方案1

  1. 创建websocket连接时,在logback中动态添加自定义Appender,将日志通过自定义Appender向websocket连接发送

    • 自定义Appender
    public class MyAppender extends AppenderBase<ILoggingEvent> {
    
    	private WebSocketServer webSocketServer;
    
    	public MyAppender(WebSocketServer webSocketServer) {
    		this.webSocketServer = webSocketServer;
    	}
    
    	/**
    	 * 添加日志
    	 * @param iLoggingEvent
    	 */
    	@Override
    	protected void append(ILoggingEvent iLoggingEvent) {
    		try {
    			webSocketServer.sendMessage(doLayout(iLoggingEvent));
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    	}
    
    	/**
    	 * 格式化日志
    	 * @param event
    	 * @return
    	 */
    	public String doLayout(ILoggingEvent event) {
    		StringBuilder sbuf = new StringBuilder();
    		if (null != event && null != event.getMDCPropertyMap()) {
    			SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
    
    			sbuf.append(simpleDateFormat.format(new Date(event.getTimeStamp())));
    			sbuf.append("\t");
    
    			sbuf.append(event.getLevel());
    			sbuf.append("\t");
    
    			sbuf.append(event.getThreadName());
    			sbuf.append("\t");
    
    			sbuf.append(event.getLoggerName());
    			sbuf.append("\t");
    
    			sbuf.append(event.getFormattedMessage().replace("\"", "\\\""));
    			sbuf.append("\t");
    		}
    
    		return sbuf.toString();
    	}
    }
    
    • 后端响应WebSocket
    @ServerEndpoint("/webSocket")
    @Component
    public class WebSocketServer {
    
    	//与某个客户端的连接会话,需要通过它来给客户端发送数据
    	private Session session;
    
    	private Integer sessionId;
    
    	/**
    	 * 连接建立成功调用的方法
    	 */
    	@OnOpen
    	public void onOpen(Session session) {
    		this.session = session;
    		this.sessionId = (new Random()).nextInt(100000);
    		LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory();
    		// 第二步:获取日志对象 (日志是有继承关系的,关闭上层,下层如果没有特殊说明也会关闭)
    		ch.qos.logback.classic.Logger rootLogger = lc.getLogger("root");
    		MyAppender myAppender = new MyAppender(this);
    		myAppender.setContext(lc);
            	// 自定义Appender设置name
    		myAppender.setName("myAppender" + sessionId);
    		myAppender.start();
    		rootLogger.addAppender(myAppender);
    		System.out.println("注入成功");
    	}
    
    	/**
    	 * 连接关闭调用的方法
    	 */
    	@OnClose
    	public void onClose() {
    		LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory();
    		ch.qos.logback.classic.Logger rootLogger = lc.getLogger("root");
            	// 通过name移除Appender
    		rootLogger.detachAppender("myAppender" + sessionId);
    		System.out.println("--------移除成功--------");
    	}
    
    	/**
    	 * 服务器主动发送消息
    	 */
    	public void sendMessage(String message) throws IOException {
    		this.session.getBasicRemote().sendText(message);
    	}
    }
    
    • 前端
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8" />
        <title>实时日志</title>
        <script src="./js/sockjs.min.js"></script>
        <script src="./js/stomp.js"></script>
        <script src="./js/jquery-3.1.1.js"></script>
    </head>
    <body>
    <noscript><h2 style="color:#ff0000">抱歉,您的浏览器不支持该功能!</h2></noscript>
    <div>
        <div>
            <button id="connect" onclick="connect();">连接</button>
            <button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接</button>
        </div>
        <div id="conversationDiv">
            <textarea id="response"></textarea>
        </div>
    </div>
    </body>
    <script type="text/javascript">
        var ws;
    
        function setConnected(connected){
            document.getElementById('connect').disabled = connected;
            document.getElementById('disconnect').disabled = !connected;
            // document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
            $("#response").html();
        }
    
        function connect(){
            ws = new WebSocket('ws://localhost:8081/webSocket');
            ws.onopen = WSonOpen;
            ws.onmessage = WSonMessage;
            ws.onclose = WSonClose;
            ws.onerror = WSonError;
        }
    
        function WSonOpen() {
            var message = {
                Content:'成功连接'
            }
            setConnected(true);
            showResponse(message)
        };
    
        function WSonMessage(event) {
            var message = {
                Content:event.data
            }
            showResponse(message)
        };
    
        function WSonClose() {
            var message = {
                Content:'连接断开'
            }
            showResponse(message)
        };
    
        function WSonError() {
            var message = {
                Content:'连接错误!'
            }
            showResponse(message)
        };
    
        function disconnect(){
            ws.close()
            setConnected(false);
        }
    
        function sendMessage(){
            ws.send(JSON.stringify({'Content':Content}))
        }
    
        function showResponse(message){
             var response = $("#response").val();
             $("#response").val(response+message.Content+'\n');
             // 一直滚到到最底部,会造成显示延迟
            var textarea = document.getElementById("response");
            textarea.scrollTop = textarea.scrollHeight;
        }
    </script>
    </html>
    
  2. 关闭websocket时,将自定义Appender从logback中移除

  • 效果

缺点:无法显示异常的堆栈信息(可通过全局异常捕获,再将异常堆栈信息通过logback打印,比较麻烦,还存在明显缺陷,不太好)

方案2

  1. 创建websocket连接时,使用Runtime创建子进程Process,子进程执行tail -f 日志文件,获取子进程输出流,将日志向websocket连接发送
    @ServerEndpoint("/realtimeLog/{level}")
    @Component
    public class RealTimeLog {
    
    	private static Logger logger = LoggerFactory.getLogger(RealTimeLog.class);
    
    	private static String logPath;
    
    	private static String webSocketSecret;
    
    	private Runnable runnable;
    
    	private Process process;
    	
    	@Autowired
    	public void setLogPath(@Value("${log.path}") String logPath) {
    		this.logPath = logPath;
    	}
    
    	ThreadPoolExecutor executor = new ThreadPoolExecutor(0, 20, 10, TimeUnit.SECONDS,
    			new LinkedBlockingQueue<>(20),
    			new CustomizableThreadFactory("realtime-log-thread-"),
    			new ThreadPoolExecutor.AbortPolicy()
    	);
    
    
    	/**
    	 * 连接建立成功调用的方法
    	 */
    	@OnOpen
    	public void onOpen(Session session, @PathParam("level") String level) throws Exception {
    		if (Strings.isNullOrEmpty(level)) {
    			level = "INFO";
    		}
    		process = Runtime.getRuntime().exec("tail -f " + logPath + "api.log");
            	// 启动新线程发送日志
    		runnable = new Runnable() {
    			@Override
    			public void run() {
    				try (BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
    					String line;
    					while ((line = in.readLine()) != null) {
    						session.getBasicRemote().sendText(line);
    					}
    				} catch (IOException e) {
    					e.printStackTrace();
    				}
    			}
    		};
    		executor.execute(runnable);
    	}
    
    	/**
    	 * 连接关闭调用的方法
    	 */
    	@OnClose
    	public void onClose() {
            // 移除线程
    		executor.remove(runnable);
    		if (process != null) {
                // 关闭子进程
    			process.destroy();
    		}
    	}
    
        	// 拼接grep过滤条件
    	public static String appendLevel(String level) {
    		StringBuffer stringBuffer = new StringBuffer();
    		String[] arr = new String[]{"DEBUG", "INFO", "WARNING", "ERROR"};
    		for (int i = 0; i < arr.length; i++) {
    			if (arr[i].equals(level)) {
    				stringBuffer.append("|" + arr[i]);
    				for (int j = i + 1; j < arr.length; j++) {
    					stringBuffer.append("|" + arr[j]);
    				}
    				break;
    			}
    		}
    		return stringBuffer.toString();
    	}
    }
    
  2. 关闭websocket时,关闭输出流,关闭子进程
  • 效果