前言
SpringBoot對(duì)所有內(nèi)部日志使用通用日志記錄,但保留底層日志實(shí)現(xiàn)。為Java Util
Logging、Log4J2和Logback提供了默認(rèn)配置。在不同的情況下,日志記錄器都預(yù)先配置為使用控制臺(tái)輸出,同時(shí)還提供可選的文件輸出。默認(rèn)情況下,SpringBoot使用Logback進(jìn)行日志記錄。
日志級(jí)別有(從高到低):FATAL(致命),ERROR(錯(cuò)誤),WARN(警告),INFO(信息),DEBUG(調(diào)試),TRACE(跟蹤)或者?OFF
(關(guān)閉),默認(rèn)的日志配置在消息寫(xiě)入時(shí)將消息回顯到控制臺(tái)。默認(rèn)情況下,將記錄錯(cuò)誤級(jí)別、警告級(jí)別和信息級(jí)別的消息。
PS:Logback does not have a?FATAL?level. It is mapped to?ERROR?
Logback沒(méi)有FATAL致命級(jí)別。它被映射到ERROR錯(cuò)誤級(jí)別
詳情請(qǐng)戳官方文檔:
https://docs.spring.io/spring-boot/docs/2.1.5.RELEASE/reference/htmlsingle/#boot-features-logging
<https://docs.spring.io/spring-boot/docs/2.1.5.RELEASE/reference/htmlsingle/#boot-features-logging>
本文主要記錄Logback日志輸出到文件以及實(shí)時(shí)輸出到web頁(yè)面
輸出到文件
我們創(chuàng)建SpringBoot項(xiàng)目時(shí),spring-boot-starter已經(jīng)包含了spring-boot-starter-logging,不需要再進(jìn)行引入依賴
標(biāo)準(zhǔn)日志格式
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.?日期和時(shí)間:毫秒精度,易于排序。
* Log Level:?ERROR,?WARN,?INFO,?DEBUG, or?TRACE.?日志級(jí)別:錯(cuò)誤、警告、信息、調(diào)試或跟蹤。
* Process ID.?進(jìn)程ID。
* A?---?separator to distinguish the start of actual log messages.?
分隔符,用于區(qū)分實(shí)際日志消息的開(kāi)始。
* Thread name: Enclosed in square brackets (may be truncated for console
output).?線程名稱:括在方括號(hào)中(可能會(huì)被截?cái)嘁杂糜诳刂婆_(tái)輸出)。
* Logger name: This is usually the source class name (often abbreviated).?
日志程序名稱:這通常是源類名稱(通常縮寫(xiě))。
* The log message.?日志消息。
如何打印日志?
方法1
/** * 配置內(nèi)部類 */ @Controller @Configuration class Config { /** *
獲取日志對(duì)象,構(gòu)造函數(shù)傳入當(dāng)前類,查找日志方便定位*/ private final Logger log = LoggerFactory.getLogger(
this.getClass()); @Value("${user.home}") private String userName; /** * 端口 */
@Value("${server.port}") private String port; /** * 啟動(dòng)成功 */ @Bean public
ApplicationRunner applicationRunner() {return applicationArguments -> { try {
InetAddress ia= InetAddress.getLocalHost(); //獲取本機(jī)內(nèi)網(wǎng)IP log.info("啟動(dòng)成功:" +
"http://" + ia.getHostAddress() + ":" + port + "/"); log.info("${user.home} :" +
userName); }catch (UnknownHostException ex) { ex.printStackTrace(); } }; } }
方法2? 使用lombok的@Slf4j,幫我們創(chuàng)建Logger對(duì)象,效果與方法1一樣
/** * 配置內(nèi)部類 */ @Slf4j @Controller @Configuration class Config { @Value(
"${user.home}") private String userName; /** * 端口 */ @Value("${server.port}")
private String port;/** * 啟動(dòng)成功 */ @Bean public ApplicationRunner
applicationRunner() {return applicationArguments -> { try { InetAddress ia =
InetAddress.getLocalHost();//獲取本機(jī)內(nèi)網(wǎng)IP log.info("啟動(dòng)成功:" + "http://" +
ia.getHostAddress() + ":" + port + "/"); log.info("${user.home} :" + userName);
}catch (UnknownHostException ex) { ex.printStackTrace(); } }; } }
?
?
簡(jiǎn)單配置
如果不需要進(jìn)行復(fù)雜的日志配置,則在配置文件中進(jìn)行簡(jiǎn)單的日志配置即可,默認(rèn)情況下,SpringBoot日志只記錄到控制臺(tái),不寫(xiě)日志文件。如果希望在控制臺(tái)輸出之外編寫(xiě)日志文件,則需要進(jìn)行配置
logging: path: /Users/Administrator/Desktop/雜七雜八/ims #日志文件路徑 file: ims.log
#日志文件名稱 level: root: info #日志級(jí)別 root表示所有包,也可以單獨(dú)配置具體包 fatal error warn info
debug trace off
?
重新啟動(dòng)項(xiàng)目
打開(kāi)ims.log
?
擴(kuò)展配置
? Spring
Boot包含許多Logback擴(kuò)展,可以幫助進(jìn)行高級(jí)配置。您可以在您的logback-spring.xml配置文件中使用這些擴(kuò)展。如果需要比較復(fù)雜的配置,建議使用擴(kuò)展配置的方式
PS:SpringBoot推薦我們使用帶-spring后綴的 logback-spring.xml
擴(kuò)展配置,因?yàn)槟J(rèn)的的logback.xml標(biāo)準(zhǔn)配置,Spring無(wú)法完全控制日志初始化。(spring擴(kuò)展對(duì)springProfile節(jié)點(diǎn)的支持)
以下是項(xiàng)目常見(jiàn)的完整logback-spring.xml,SpringBoot默認(rèn)掃描classpath下面的logback.xml、logback-spring.xml,所以不需要再指定spring.logging.config,當(dāng)然,你指定也沒(méi)有問(wèn)題
<?xml version="1.0" encoding="UTF-8"?> <configuration debug="false"> <!--
日志文件主目錄:這里${user.home}為當(dāng)前服務(wù)器用戶主目錄--> <property name="LOG_HOME" value
="${user.home}/log"/> <!--日志文件名稱:這里spring.application.name表示工程名稱--> <
springPropertyscope="context" name="APP_NAME" source="spring.application.name"/>
<!--默認(rèn)配置--> <include resource
="org/springframework/boot/logging/logback/defaults.xml"/> <!--配置控制臺(tái)(Console)-->
<include resource
="org/springframework/boot/logging/logback/console-appender.xml"/> <!--
配置日志文件(File)--> <appender name="FILE" class
="ch.qos.logback.core.rolling.RollingFileAppender"> <!--設(shè)置策略--> <rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!--
日志文件路徑:這里%d{yyyyMMdd}表示按天分類日志--> <FileNamePattern>
${LOG_HOME}/%d{yyyyMMdd}/${APP_NAME}.log</FileNamePattern> <!--日志保留天數(shù)--> <
MaxHistory>15</MaxHistory> </rollingPolicy> <!--設(shè)置格式--> <encoder class
="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--
格式化輸出:%d表示日期,%thread表示線程名,%-5level:級(jí)別從左顯示5個(gè)字符寬度%msg:日志消息,%n是換行符--> <pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<!-- 或者使用默認(rèn)配置 --> <!--<pattern>${FILE_LOG_PATTERN}</pattern>--> <charset>utf8</
charset> </encoder> <!--日志文件最大的大小--> <triggeringPolicy class
="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> <MaxFileSize>100MB</
MaxFileSize> </triggeringPolicy> </appender> <!-- 多環(huán)境配置 按照active profile選擇分支 -->
<springProfile name="dev"> <!--root節(jié)點(diǎn) 全局日志級(jí)別,用來(lái)指定最基礎(chǔ)的日志輸出級(jí)別--> <root level
="INFO"> <appender-ref ref="FILE"/> <appender-ref ref="CONSOLE"/> </root> <!--
子節(jié)點(diǎn)向上級(jí)傳遞 局部日志級(jí)別--> <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
>
啟動(dòng)項(xiàng)目,去到${user.home}當(dāng)前服務(wù)器用戶主目錄,日志按日期進(jìn)行產(chǎn)生,如果項(xiàng)目產(chǎn)生的日志文件比較大,還可以按照小時(shí)進(jìn)行.log文件的生成
?
當(dāng)然,使用簡(jiǎn)單配置照樣能進(jìn)行按日期分類
logging: path: ${user.home}/log/%d{yyyyMMdd} #日志文件路徑
這里${user.home}為當(dāng)前服務(wù)器用戶主目錄 file: ${spring.application.name}.log #日志文件名稱
${spring.application.name}為應(yīng)用名 level: root: info #日志級(jí)別 root表示所有包,也可以單獨(dú)配置具體包
fatal error warn info debug trace off
?
輸出到Web頁(yè)面
我們已經(jīng)有日志文件.log了,為什么還要這個(gè)功能呢?(滑稽臉)為了偷懶!
當(dāng)我們把項(xiàng)目部署到Linux服務(wù)器,當(dāng)你想看日志文件,還得打開(kāi)xshell連接,定位到log文件夾,麻煩;如果我們把日志輸出到Web頁(yè)面,當(dāng)做超級(jí)管理員或者測(cè)試賬號(hào)下面的一個(gè)功能,點(diǎn)擊就開(kāi)始實(shí)時(shí)獲取生成的日志并輸出在Web頁(yè)面,是不是爽很多呢?
PS:這個(gè)功能可得小心使用,因?yàn)槿罩緯?huì)暴露很多信息
?
LoggingWSServer
使用WebSocket實(shí)現(xiàn)實(shí)時(shí)獲取,建立WebSocket連接后創(chuàng)建一個(gè)線程任務(wù),每秒讀取一次最新的日志文件,第一次只取后面200行,后面取相比上次新增的行,為了在頁(yè)面上更加方便的閱讀日志,對(duì)日志級(jí)別單詞進(jìn)行著色(PS:如何創(chuàng)建springboot的websocket,請(qǐng)戳:
SpringBoot系列——WebSocket <https://www.cnblogs.com/huanzi-qch/p/9952578.html>)
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獲取實(shí)時(shí)日志并輸出到Web頁(yè)面 */ @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>(); /** * 連接建立成功調(diào)用的方法 */ @OnOpen public void
onOpen(Session session) {//添加到集合中 sessionMap.put(session.getId(), session);
lengthMap.put(session.getId(),1);//默認(rèn)從第一行開(kāi)始 //獲取日志信息 new Thread(() -> {
log.info("LoggingWebSocketServer 任務(wù)開(kāi)始"); 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();//只取從上次之后產(chǎn)生的日志 Object[] copyOfRange =
Arrays.copyOfRange(lines, lengthMap.get(session.getId()), lines.length);//
對(duì)日志進(jìn)行著色,更加美觀 PS:注意,這里要根據(jù)日志生成規(guī)則來(lái)操作 for (int i = 0; i < copyOfRange.length; i++)
{ String line= (String) copyOfRange[i]; //先轉(zhuǎn)義 line = line.replaceAll("&",
"&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll("\"",
"""); //處理等級(jí) 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; } //存儲(chǔ)最新一行開(kāi)始
lengthMap.put(session.getId(), lines.length);//第一次如果太大,截取最新的200行就夠了,避免傳輸?shù)臄?shù)據(jù)太大 if
(first && copyOfRange.length > 200){ copyOfRange =
Arrays.copyOfRange(copyOfRange, copyOfRange.length - 200, copyOfRange.length);
first= false; } String result = StringUtils.join(copyOfRange, "<br/>"); //發(fā)送
send(session, result);//休眠一秒 Thread.sleep(1000); } catch (Exception e) { //
捕獲但不處理 e.printStackTrace(); } finally { try { reader.close(); } catch
(IOException ignored) { } } } log.info("LoggingWebSocketServer 任務(wù)結(jié)束");
}).start(); }/** * 連接關(guān)閉調(diào)用的方法 */ @OnClose public void onClose(Session session) {
//從集合中刪除 sessionMap.remove(session.getId());
lengthMap.remove(session.getId()); }/** * 發(fā)生錯(cuò)誤時(shí)調(diào)用 */ @OnError public void
onError(Session session, Throwable error) { error.printStackTrace(); }/** *
服務(wù)器接收到客戶端消息時(shí)調(diào)用的方法*/ @OnMessage public void onMessage(String message, Session
session) { }/** * 封裝一個(gè)send方法,發(fā)送消息到前端 */ private void send(Session session,
String message) {try { session.getBasicRemote().sendText(message); } catch
(Exception e) { e.printStackTrace(); } } }
?
HTML頁(yè)面
頁(yè)面收到數(shù)據(jù)就追加到div中,為了方便新增了幾個(gè)功能:
清屏,清空div內(nèi)容
滾動(dòng)至底部、將div的滾動(dòng)條滑到最下面
開(kāi)啟/關(guān)閉自動(dòng)滾動(dòng),div新增內(nèi)容后自動(dòng)將滾動(dòng)條滑到最下面,點(diǎn)一下開(kāi)啟,再點(diǎn)關(guān)閉,默認(rèn)關(guān)閉
PS:引入公用部分,就是一些jquery等常用靜態(tài)資源
<!DOCTYPE> <!--解決idea thymeleaf 表達(dá)式模板報(bào)紅波浪線--> <!--suppress ALL --> <html
xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>
IMS實(shí)時(shí)日志</title> <!-- 引入公用部分 --> <script th:replace="head::static"></script> </
head> <body> <!-- 標(biāo)題 --> <h1 style="text-align: center;">IMS實(shí)時(shí)日志</h1> <!-- 顯示區(qū)
--> <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;">滾動(dòng)至底部 </button> <button onclick
="if(window.loggingAutoBottom){$(this).text('開(kāi)啟自動(dòng)滾動(dòng)');}else{$(this).text('關(guān)閉自動(dòng)滾動(dòng)');};window.loggingAutoBottom
= !window.loggingAutoBottom" style="color: green; height: 35px; ">開(kāi)啟自動(dòng)滾動(dòng) </
button> </div> </body> <script th:inline="javascript"> //websocket對(duì)象 let
websocket= null; //判斷當(dāng)前瀏覽器是否支持WebSocket if ('WebSocket' in window) { websocket =
new WebSocket("ws://localhost:10086/websocket/logging"); } else { console.error(
"不支持WebSocket"); } //連接發(fā)生錯(cuò)誤的回調(diào)方法 websocket.onerror = function (e) {
console.error("WebSocket連接發(fā)生錯(cuò)誤"); }; //連接成功建立的回調(diào)方法 websocket.onopen = function
() { console.log("WebSocket連接成功") }; //接收到消息的回調(diào)方法 websocket.onmessage =
function (event) { //追加 if (event.data) { //日志內(nèi)容 let $loggingText = $("
#loggingText"); $loggingText.append(event.data); //是否開(kāi)啟自動(dòng)底部 if
(window.loggingAutoBottom) {//滾動(dòng)條自動(dòng)到最底部 $loggingText.scrollTop($loggingText[0
].scrollHeight); } } }//連接關(guān)閉的回調(diào)方法 websocket.onclose = function () {
console.log("WebSocket連接關(guān)閉") }; </script> </html>
?
效果展示
?
后記
有了日志記錄,我們以后寫(xiě)代碼時(shí)就要注意了,應(yīng)使用下面的正確示例
//錯(cuò)誤示例,這樣寫(xiě)只會(huì)輸出到控制臺(tái),不會(huì)輸出到日志中 System.out.println("XXX"); e.printStackTrace(); //
正確示例,既輸出到控制臺(tái),又輸出到日志 log.info("XXX"); log.error("XXX報(bào)錯(cuò)",e);
?
SpringBoot日志暫時(shí)先記錄到這里,點(diǎn)擊官網(wǎng)了解更多:
https://docs.spring.io/spring-boot/docs/2.1.5.RELEASE/reference/htmlsingle/#boot-features-logging
<https://docs.spring.io/spring-boot/docs/2.1.5.RELEASE/reference/htmlsingle/#boot-features-logging>
?
補(bǔ)充
2019-07-03補(bǔ)充:我們之前只對(duì)日志等級(jí)關(guān)鍵字進(jìn)行著色,還是覺(jué)得不夠,因此又新增了類名著色跟HTML轉(zhuǎn)義
? 主要修改:
效果:
?
?
2019-08-12補(bǔ)充:我發(fā)現(xiàn)有時(shí)候顯示的時(shí)候,換行不太準(zhǔn)確,我們?cè)仁窃谛心┳芳?lt;br/>,但有時(shí)候讀取出來(lái)的一行記錄是自動(dòng)換行后的數(shù)據(jù),頁(yè)面顯示效果很丑
?
?
因此我改成用正則([\d+][\d+][\d+][\d+]-[\d+][\d+]-[\d+][\d+]
[\d+][\d+]:[\d+][\d+]:[\d+][\d+])去匹配日期,然后再對(duì)應(yīng)的起始下標(biāo)插入<br/>,從而達(dá)到與控制臺(tái)輸出類似的效果
? 匹配、插入結(jié)果
?
頁(yè)面效果
?
異步輸出日志
異步輸出日志的方式很簡(jiǎn)單,添加一個(gè)基于異步寫(xiě)日志的appender,并指向原先配置的appender即可
<!-- 將文件輸出設(shè)置成異步輸出 --> <appender name="ASYNC-FILE" class
="ch.qos.logback.classic.AsyncAppender"> <!--
不丟失日志.默認(rèn)的,如果隊(duì)列的80%已滿,則會(huì)丟棄TRACT、DEBUG、INFO級(jí)別的日志--> <discardingThreshold>0</
discardingThreshold> <!-- 更改默認(rèn)的隊(duì)列的深度,該值會(huì)影響性能.默認(rèn)值為256 --> <queueSize>256</
queueSize> <!-- 添加附加的appender,最多只能添加一個(gè) --> <appender-ref ref="FILE"/> </appender
> <!-- 將控制臺(tái)輸出設(shè)置成異步輸出 --> <appender name="ASYNC-CONSOLE" class
="ch.qos.logback.classic.AsyncAppender"> <!--
不丟失日志.默認(rèn)的,如果隊(duì)列的80%已滿,則會(huì)丟棄TRACT、DEBUG、INFO級(jí)別的日志--> <discardingThreshold>0</
discardingThreshold> <!-- 更改默認(rèn)的隊(duì)列的深度,該值會(huì)影響性能.默認(rèn)值為256 --> <queueSize>256</
queueSize> <!-- 添加附加的appender,最多只能添加一個(gè) --> <appender-ref ref="CONSOLE"/> </
appender>
原理很簡(jiǎn)單,主線程將日志扔到阻塞隊(duì)列中,然后IO操作日志寫(xiě)入文件是通過(guò)新起一個(gè)線程去完成的
?
?
?
代碼開(kāi)源
代碼已經(jīng)開(kāi)源、托管到我的GitHub、碼云:
GitHub:https://github.com/huanzi-qch/springBoot
<https://github.com/huanzi-qch/springBoot>
碼云:https://gitee.com/huanzi-qch/springBoot
<https://gitee.com/huanzi-qch/springBoot>
?
熱門工具 換一換
