方案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>
-
关闭websocket时,将自定义Appender从logback中移除
- 效果
缺点:无法显示异常的堆栈信息(可通过全局异常捕获,再将异常堆栈信息通过logback打印,比较麻烦,还存在明显缺陷,不太好)
方案2
- 创建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(); } }
- 关闭websocket时,关闭输出流,关闭子进程
- 效果