前言
SpringBoot對所有內部日志使用通用日志記錄,但保留底層日志實現。為Java Util Logging、Log4J2和Logback提供了默認配置。在不同的情況下,日志記錄器都預先配置為使用控制台輸出,同時還提供可選的文件輸出。默認情況下,SpringBoot使用Logback進行日志記錄。
日志級別有(從高到低):FATAL(致命),ERROR(錯誤),WARN(警告),INFO(信息),DEBUG(調試),TRACE(跟蹤)或者 OFF(關閉),默認的日志配置在消息寫入時將消息回顯到控制台。默認情況下,將記錄錯誤級別、警告級別和信息級別的消息。
PS:Logback does not have a FATAL level. It is mapped to 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日志只記錄到控制台,不寫日志文件。如果希望在控制台輸出之外編寫日志文件,則需要進行配置
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文件的生成


當然,使用簡單配置照樣能進行按日期分類
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:這個功能可得小心使用,因為日志會暴露很多信息
LoggingWSServer
使用WebSocket實現實時獲取,建立WebSocket連接后創建一個線程任務,每秒讀取一次最新的日志文件,第一次只取后面200行,后面取相比上次新增的行,為了在頁面上更加方便的閱讀日志,對日志級別單詞進行着色(PS:如何創建springboot的websocket,請戳:SpringBoot系列——WebSocket)
package cn.huanzi.qch.springbootlogback;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
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.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket獲取實時日志並輸出到Web頁面
*/
@Slf4j
@Component
@ServerEndpoint(value = "/websocket/logging", configurator = MyEndpointConfigure.class)
public class LoggingWSServer {
@Value("${spring.application.name}")
private String applicationName;
/**
* 連接集合
*/
private static Map<String, Session> sessionMap = new ConcurrentHashMap<String, Session>();
private static Map<String, Integer> lengthMap = new ConcurrentHashMap<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()) + "/"+applicationName+".log";
//字符流
reader = new BufferedReader(new FileReader(filePath));
Object[] lines = reader.lines().toArray();
//只取從上次之后產生的日志
Object[] copyOfRange = Arrays.copyOfRange(lines, lengthMap.get(session.getId()), lines.length);
//對日志進行着色,更加美觀 PS:注意,這里要根據日志生成規則來操作
for (int i = 0; i < copyOfRange.length; i++) {
String line = (String) copyOfRange[i];
//先轉義
line = line.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll("\"", """);
//處理等級
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>");
//處理類名
String[] split = line.split("]");
if (split.length >= 2) {
String[] split1 = split[1].split("-");
if (split1.length >= 2) {
line = split[0] + "]" + "<span style='color: #298a8a;'>" + split1[0] + "</span>" + "-" + split1[1];
}
}
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) {
//從集合中刪除
sessionMap.remove(session.getId());
lengthMap.remove(session.getId());
}
/**
* 發生錯誤時調用
*/
@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; 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-logging
補充
2019-07-03補充:我們之前只對日志等級關鍵字進行着色,還是覺得不夠,因此又新增了類名着色跟HTML轉義
主要修改:

效果:


2019-08-12補充:我發現有時候顯示的時候,換行不太准確,我們原先是在行末追加<br/>,但有時候讀取出來的一行記錄是自動換行后的數據,頁面顯示效果很丑

因此我改成用正則([\d+][\d+][\d+][\d+]-[\d+][\d+]-[\d+][\d+] [\d+][\d+]:[\d+][\d+]:[\d+][\d+])去匹配日期,然后再對應的起始下標插入<br/>,從而達到與控制台輸出類似的效果

匹配、插入結果
頁面效果

異步輸出日志
異步輸出日志的方式很簡單,添加一個基於異步寫日志的appender,並指向原先配置的appender即可
<!-- 將文件輸出設置成異步輸出 -->
<appender name="ASYNC-FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丟失日志.默認的,如果隊列的80%已滿,則會丟棄TRACT、DEBUG、INFO級別的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默認的隊列的深度,該值會影響性能.默認值為256 -->
<queueSize>256</queueSize>
<!-- 添加附加的appender,最多只能添加一個 -->
<appender-ref ref="FILE"/>
</appender>
<!-- 將控制台輸出設置成異步輸出 -->
<appender name="ASYNC-CONSOLE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丟失日志.默認的,如果隊列的80%已滿,則會丟棄TRACT、DEBUG、INFO級別的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默認的隊列的深度,該值會影響性能.默認值為256 -->
<queueSize>256</queueSize>
<!-- 添加附加的appender,最多只能添加一個 -->
<appender-ref ref="CONSOLE"/>
</appender>
原理很簡單,主線程將日志扔到阻塞隊列中,然后IO操作日志寫入文件是通過新起一個線程去完成的
2020-05-26補充
e.printStackTrace();會打出詳細異常,異常名稱,出錯位置,便於調試用,但直接調用會輸出到std.err,並沒有輸出到日志文件中,因此需要先輸出到流中再轉成字符串
封裝工具類
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* 捕獲報錯日志處理工具類
*/
public class ErrorUtil {
/**
* Exception出錯的棧信息轉成字符串
* 用於打印到日志中
*/
public static String errorInfoToString(Throwable e) {
StringWriter sw = null;
PrintWriter pw = null;
try {
sw = new StringWriter();
pw = new PrintWriter(sw);
// 將出錯的棧信息輸出到printWriter中
e.printStackTrace(pw);
pw.flush();
sw.flush();
} finally {
if (sw != null) {
try {
sw.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (pw != null) {
pw.close();
}
}
return sw.toString();
}
}
使用
try {
//省略其他代碼
} catch (Throwable e) {
//之前的操作,輸出控制台
e.printStackTrace();
//輸出到日志文件中
log.error(ErrorUtil.errorInfoToString(e));
}

