SpringBoot系列——Logback日誌,輸出到檔案以及實時輸出到web頁面
前言
SpringBoot對所有內部日誌使用通用日誌記錄,但保留底層日誌實現。為Java Util Logging、Log4J2和Logback提供了預設配置。在不同的情況下,日誌記錄器都預先配置為使用控制檯輸出,同時還提供可選的檔案輸出。預設情況下,SpringBoot使用Logback進行日誌記錄。
日誌級別有(從高到低):FATAL(致命),ERROR
(
錯誤),
WARN
(
警告),
INFO
(
資訊),
DEBUG
(
除錯),
TRACE
(
跟蹤)或者 OFF
(關閉),預設的日誌配置在訊息寫入時將訊息回顯到控制檯。預設情況下,將記錄錯誤級別、警告級別和資訊級別的訊息。
PS:Logback does not have a FATAL
ERROR Logback沒有FATAL致命級別。它被對映到ERROR錯誤級別
詳情請戳官方文件:https://docs.spring.io/spring-boot/docs/2.1.5.RELEASE/reference/htmlsingle/#boot-features-logging
本文主要記錄Logback日誌輸出到檔案以及實時輸出到web頁面
輸出到檔案
我們建立SpringBoot專案時,spring-boot-starter已經包含了spring-boot-starter-logging,不需要再進行引入依賴
標準日誌格式
2014-03-05 10:57:51.112 INFO 45469 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/7.0.52 2014-03-05 10:57:51.253 INFO 45469 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2014-03-05 10:57:51.253 INFO 45469 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1358 ms 2014-03-05 10:57:51.698 INFO 45469 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/] 2014-03-05 10:57:51.702 INFO 45469 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
- Date and Time: Millisecond precision and easily sortable. 日期和時間:毫秒精度,易於排序。
- Log Level:
ERROR
,WARN
,INFO
,DEBUG
, orTRACE
. 日誌級別:錯誤、警告、資訊、除錯或跟蹤。 - Process ID. 程序ID。
- A
---
separator to distinguish the start of actual log messages. 分隔符,用於區分實際日誌訊息的開始。 - Thread name: Enclosed in square brackets (may be truncated for console output). 執行緒名稱:括在方括號中(可能會被截斷以用於控制檯輸出)。
- Logger name: This is usually the source class name (often abbreviated). 日誌程式名稱:這通常是源類名稱(通常縮寫)。
- The log message. 日誌訊息。
如何列印日誌?
方法1
/** * 配置內部類 */ @Controller @Configuration class Config { /** * 獲取日誌物件,建構函式傳入當前類,查詢日誌方便定位 */ private final Logger log = LoggerFactory.getLogger(this.getClass()); @Value("${user.home}") private String userName; /** * 埠 */ @Value("${server.port}") private String port; /** * 啟動成功 */ @Bean public ApplicationRunner applicationRunner() { return applicationArguments -> { try { InetAddress ia = InetAddress.getLocalHost(); //獲取本機內網IP log.info("啟動成功:" + "http://" + ia.getHostAddress() + ":" + port + "/"); log.info("${user.home} :" + userName); } catch (UnknownHostException ex) { ex.printStackTrace(); } }; } }
方法2 使用lombok的@Slf4j,幫我們建立Logger物件,效果與方法1一樣
/** * 配置內部類 */ @Slf4j @Controller @Configuration class Config { @Value("${user.home}") private String userName; /** * 埠 */ @Value("${server.port}") private String port;/** * 啟動成功 */ @Bean public ApplicationRunner applicationRunner() { return applicationArguments -> { try { InetAddress ia = InetAddress.getLocalHost(); //獲取本機內網IP log.info("啟動成功:" + "http://" + ia.getHostAddress() + ":" + port + "/"); log.info("${user.home} :" + userName); } catch (UnknownHostException ex) { ex.printStackTrace(); } }; } }
簡單配置
如果不需要進行復雜的日誌配置,則在配置檔案中進行簡單的日誌配置即可,預設情況下,SpringBoot日誌只記錄到控制檯,不寫日誌檔案。如果希望在控制檯輸出之外編寫日誌檔案,則需要進行配置
spring: logging: path: /Users/Administrator/Desktop/雜七雜八/ims #日誌檔案路徑 file: ims.log #日誌檔名稱 level: root: info #日誌級別 root表示所有包,也可以單獨配置具體包 fatal error warn info debug trace off
重新啟動專案
開啟ims.log
擴充套件配置
Spring Boot包含許多Logback擴充套件,可以幫助進行高階配置。您可以在您的logback-spring.xml配置檔案中使用這些擴充套件。如果需要比較複雜的配置,建議使用擴充套件配置的方式
PS:SpringBoot推薦我們使用帶-spring字尾的 logback-spring.xml 擴充套件配置,因為預設的的logback.xml標準配置,Spring無法完全控制日誌初始化。(spring擴充套件對springProfile節點的支援)
以下是專案常見的完整logback-spring.xml,SpringBoot預設掃描classpath下面的logback.xml、logback-spring.xml,所以不需要再指定spring.logging.config,當然,你指定也沒有問題
<?xml version="1.0" encoding="UTF-8"?> <configuration debug="false"> <!--日誌檔案主目錄:這裡${user.home}為當前伺服器使用者主目錄--> <property name="LOG_HOME" value="${user.home}/log"/> <!--日誌檔名稱:這裡spring.application.name表示工程名稱--> <springProperty scope="context" name="APP_NAME" source="spring.application.name"/> <!--預設配置--> <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <!--配置控制檯(Console)--> <include resource="org/springframework/boot/logging/logback/console-appender.xml"/> <!--配置日誌檔案(File)--> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!--設定策略--> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!--日誌檔案路徑:這裡%d{yyyyMMdd}表示按天分類日誌--> <FileNamePattern>${LOG_HOME}/%d{yyyyMMdd}/${APP_NAME}.log</FileNamePattern> <!--日誌保留天數--> <MaxHistory>15</MaxHistory> </rollingPolicy> <!--設定格式--> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--格式化輸出:%d表示日期,%thread表示執行緒名,%-5level:級別從左顯示5個字元寬度%msg:日誌訊息,%n是換行符--> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> <!-- 或者使用預設配置 --> <!--<pattern>${FILE_LOG_PATTERN}</pattern>--> <charset>utf8</charset> </encoder> <!--日誌檔案最大的大小--> <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> <MaxFileSize>100MB</MaxFileSize> </triggeringPolicy> </appender> <!-- 多環境配置 按照active profile選擇分支 --> <springProfile name="dev"> <!--root節點 全域性日誌級別,用來指定最基礎的日誌輸出級別--> <root level="INFO"> <appender-ref ref="FILE"/> <appender-ref ref="CONSOLE"/> </root> <!-- 子節點向上級傳遞 區域性日誌級別--> <logger level="WARN" name="org.springframework"/> <logger level="WARN" name="com.netflix"/> <logger level="DEBUG" name="org.hibernate.SQL"/> </springProfile> <springProfile name="prod"> </springProfile> </configuration>
啟動專案,去到${user.home}當前伺服器使用者主目錄,日誌按日期進行產生,如果專案產生的日誌檔案比較大,還可以按照小時進行.log檔案的生成
當然,使用簡單配置照樣能進行按日期分類
spring:
logging: path: ${user.home}/log/%d{yyyyMMdd} #日誌檔案路徑 這裡${user.home}為當前伺服器使用者主目錄 file: ${spring.application.name}.log #日誌檔名稱 ${spring.application.name}為應用名 level: root: info #日誌級別 root表示所有包,也可以單獨配置具體包 fatal error warn info debug trace off
輸出到Web頁面
我們已經有日誌檔案.log了,為什麼還要這個功能呢?(滑稽臉)為了偷懶!
當我們把專案部署到Linux伺服器,當你想看日誌檔案,還得開啟xshell連線,定位到log資料夾,麻煩;如果我們把日誌輸出到Web頁面,當做超級管理員或者測試賬號下面的一個功能,點選就開始實時獲取生成的日誌並輸出在Web頁面,是不是爽很多呢?
PS:這個功能可得小心使用,因為日誌會暴露很多資訊
LoggingWebSocketServer
使用WebSocket實現實時獲取,建立WebSocket連線後建立一個執行緒任務,每秒讀取一次最新的日誌檔案,第一次只取後面200行,後面取相比上次新增的行,為了在頁面上更加方便的閱讀日誌,對日誌級別單詞進行著色(PS:如何建立springboot的websocket,請戳:SpringBoot系列——WebSocket)
package cn.huanzi.ims.socket; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thymeleaf.util.StringUtils; import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * WebSocket獲取實時日誌並輸出到Web頁面 */ @Slf4j @Component @ServerEndpoint(value = "/websocket/logging", configurator = MyEndpointConfigure.class) public class LoggingWebSocketServer { /** * 連線集合 */ private static Map<String, Session> sessionMap = new HashMap<String, Session>(); private static Map<String, Integer> lengthMap = new HashMap<String, Integer>(); /** * 連線建立成功呼叫的方法 */ @OnOpen public void onOpen(Session session) { //新增到集合中 sessionMap.put(session.getId(), session); lengthMap.put(session.getId(), 1);//預設從第一行開始 //獲取日誌資訊 new Thread(() -> { log.info("LoggingWebSocketServer 任務開始"); boolean first = true; while (sessionMap.get(session.getId()) != null) { BufferedReader reader = null; try { //日誌檔案路徑,獲取最新的 String filePath = System.getProperty("user.home") + "/log/" + new SimpleDateFormat("yyyyMMdd").format(new Date()) + "/ims.log"; //字元流 reader = new BufferedReader(new FileReader(filePath)); Object[] lines = reader.lines().toArray(); //只取從上次之後產生的日誌 Object[] copyOfRange = Arrays.copyOfRange(lines, lengthMap.get(session.getId()), lines.length); for (int i = 0; i < copyOfRange.length; i++) { String line = (String) copyOfRange[i]; //按等級標顏色,更加美觀 line = line.replace("DEBUG", "<span style='color: blue;'>DEBUG</span>"); line = line.replace("INFO", "<span style='color: green;'>INFO</span>"); line = line.replace("WARN", "<span style='color: orange;'>WARN</span>"); line = line.replace("ERROR", "<span style='color: red;'>ERROR</span>"); copyOfRange[i] = line; } //儲存最新一行開始 lengthMap.put(session.getId(), lines.length); //第一次如果太大,擷取最新的200行就夠了,避免傳輸的資料太大 if(first && copyOfRange.length > 200){ copyOfRange = Arrays.copyOfRange(copyOfRange, copyOfRange.length - 200, copyOfRange.length); first = false; } String result = StringUtils.join(copyOfRange, "<br/>"); //傳送 send(session, result); //休眠一秒 Thread.sleep(1000); } catch (Exception e) { //捕獲但不處理 e.printStackTrace(); } finally { try { reader.close(); } catch (IOException ignored) { } } } log.info("LoggingWebSocketServer 任務結束"); }).start(); } /** * 連線關閉呼叫的方法 */ @OnClose public void onClose(Session session) { //從集合中刪除 for (Map.Entry<String, Session> entry : sessionMap.entrySet()) { if (entry.getValue() == session) { sessionMap.remove(entry.getKey()); lengthMap.remove(entry.getKey()); break; } } } /** * 發生錯誤時呼叫 */ @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } /** * 伺服器接收到客戶端訊息時呼叫的方法 */ @OnMessage public void onMessage(String message, Session session) { } /** * 封裝一個send方法,傳送訊息到前端 */ private void send(Session session, String message) { try { session.getBasicRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } }
HTML頁面
頁面收到資料就追加到div中,為了方便新增了幾個功能:
清屏,清空div內容
滾動至底部、將div的滾動條滑到最下面
開啟/關閉自動滾動,div新增內容後自動將滾動條滑到最下面,點一下開啟,再點關閉,預設關閉
PS:引入公用部分,就是一些jquery等常用靜態資源
<!DOCTYPE> <!--解決idea thymeleaf 表示式模板報紅波浪線--> <!--suppress ALL --> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>IMS實時日誌</title> <!-- 引入公用部分 --> <script th:replace="head::static"></script> </head> <body> <!-- 標題 --> <h1 style="text-align: center;">IMS實時日誌</h1> <!-- 顯示區 --> <div id="loggingText" contenteditable="true" style="width:100%;height: 600px;background-color: ghostwhite; overflow: auto;"></div> <!-- 操作欄 --> <div style="text-align: center;"> <button onclick="$('#loggingText').text('')" style="color: green; height: 35px;">清屏</button> <button onclick="$('#loggingText').animate({scrollTop:$('#loggingText')[0].scrollHeight});" style="color: green; height: 35px;">滾動至底部 </button> <button onclick="if(window.loggingAutoBottom){$(this).text('開啟自動滾動');}else{$(this).text('關閉自動滾動');};window.loggingAutoBottom = !window.loggingAutoBottom" style="color: green; height: 35px; ">開啟自動滾動 </button> </div> </body> <script th:inline="javascript"> //websocket物件 let websocket = null; //判斷當前瀏覽器是否支援WebSocket if ('WebSocket' in window) { websocket = new WebSocket("ws://localhost:10086/websocket/logging"); } else { console.error("不支援WebSocket"); } //連線發生錯誤的回撥方法 websocket.onerror = function (e) { console.error("WebSocket連線發生錯誤"); }; //連線成功建立的回撥方法 websocket.onopen = function () { console.log("WebSocket連線成功") }; //接收到訊息的回撥方法 websocket.onmessage = function (event) { //追加 if (event.data) { //日誌內容 let $loggingText = $("#loggingText"); $loggingText.append(event.data); //是否開啟自動底部 if (window.loggingAutoBottom) { //滾動條自動到最底部 $loggingText.scrollTop($loggingText[0].scrollHeight); } } } //連線關閉的回撥方法 websocket.onclose = function () { console.log("WebSocket連線關閉") }; </script> </html>
效果展示
後記
有了日誌記錄,我們以後寫程式碼時就要注意了,應使用下面的正確示例
//錯誤示例,這樣寫只會輸出到控制檯,不會輸出到日誌中 System.out.println("XXX"); e.printStackTrace(); //正確示例,既輸出到控制檯,又輸出到日誌 log.info("XXX"); log.error("XXX報錯",e);
SpringBoot日誌暫時先記錄到這裡,點選官網瞭解更多:https://docs.spring.io/spring-boot/docs/2.1.5.RELEASE/reference/htmlsingle/#boot-features-log