前言

            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>

          ?

          友情鏈接
          ioDraw流程圖
          API參考文檔
          OK工具箱
          云服務(wù)器優(yōu)惠
          阿里云優(yōu)惠券
          騰訊云優(yōu)惠券
          京東云優(yōu)惠券
          站點(diǎn)信息
          問(wèn)題反饋
          郵箱:[email protected]
          QQ群:637538335
          關(guān)注微信

                天天综合激情 | 国产公妇仑在线观看 | 看亚州成人AV片 | 国产一级a毛一级a看… | 日韩无码国产精品 |