SpringBoot系列——Logback日誌,輸出到文件以及實時輸出到web頁面

  前言

  SpringBoot對全部內部日誌使用通用日誌記錄,但保留底層日誌實現。爲Java Util Logging、Log4J2和Logback提供了默認配置。在不一樣的狀況下,日誌記錄器都預先配置爲使用控制檯輸出,同時還提供可選的文件輸出。默認狀況下,SpringBoot使用Logback進行日誌記錄。javascript

  日誌級別有(從高到低):FATAL(致命),ERROR錯誤),WARN警告),INFO信息),DEBUG調試),TRACE跟蹤)或者 OFF(關閉),默認的日誌配置在消息寫入時將消息回顯到控制檯。默認狀況下,將記錄錯誤級別、警告級別和信息級別的消息。html

  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-loggingjava

  本文主要記錄Logback日誌輸出到文件以及實時輸出到web頁面jquery

  

  輸出到文件

  咱們建立SpringBoot項目時,spring-boot-starter已經包含了spring-boot-starter-logging,不須要再進行引入依賴git

  標準日誌格式

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: ERRORWARNINFODEBUG, or TRACE日誌級別:錯誤、警告、信息、調試或跟蹤。
  • Process ID. 進程ID。
  • --- 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. 日誌消息。

  

  如何打印日誌?github

  方法1web

    /**
     * 配置內部類
     */
    @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同樣spring

    /**
     * 配置內部類
     */
    @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日誌只記錄到控制檯,不寫日誌文件。若是但願在控制檯輸出以外編寫日誌文件,則須要進行配置shell

  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("&", "&amp;")
                                .replaceAll("<", "&lt;")
                                .replaceAll(">", "&gt;")
                                .replaceAll("\"", "&quot;");

                        //處理等級
                        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;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-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操做日誌寫入文件是經過新起一個線程去完成的

 

 

 

  代碼開源

  代碼已經開源、託管到個人GitHub、碼雲:

  GitHub:https://github.com/huanzi-qch/springBoot

  碼雲:https://gitee.com/huanzi-qch/springBoot

相關文章
相關標籤/搜索