動態修改紀錄檔級別,太有用了!

2023-08-25 12:01:01

背景

我們在系統中一般都會列印一些紀錄檔,並且在開發、測試、生產各個環境中的紀錄檔級別可能不一樣。在開發過程中為了方便偵錯列印了很多debug紀錄檔,但是生產環境為了效能,為了節約儲存資源,我們會將紀錄檔級別設定為info或error較高的級別,只保留一些關鍵的必要的紀錄檔。

當線上出現問題需要排查時,最有效的方式是分析系統紀錄檔。此時因為線上環境紀錄檔級別較高,對排查問題有一定的阻礙,為了快速響應線上問題,我們需要更全面的紀錄檔幫助排查問題,傳統的做法是修改紀錄檔級別重啟專案。

目標

為了兼顧效能和快速響應線上問題,實現不重啟專案的前提下動態修改紀錄檔級別。通過使用該功能,可以在需要解決線上問題時,實時調整線上紀錄檔輸出級別,獲取全面的Debug紀錄檔,幫助工程師提高定位問題的效率。

技術方案

本文列舉了幾種實現方案,已經驗證可用,供大家參考。

方案一、LoggingSystem

在Spring Boot專案中可以通過LoggingSystem來獲取或修改紀錄檔設定。

1.1 獲取紀錄檔Logger設定

通過LoggingSystem API getLoggerConfigurations獲取所有Logger設定

List loggerConfigs = loggingSystem.getLoggerConfigurations();

1.2 修改紀錄檔級別

通過呼叫LoggingSystem API setLogLevel設定包或具體Logger的紀錄檔級別,修改成功,立即生效。

@Autowired
private LoggingSystem loggingSystem;

@RequestMapping(value = "/changeLogLevel", method = RequestMethod.POST)
public void changeLogLevel(String loggerName, String newLevel) {
    log.info("更新紀錄檔級別:{}", newLevel);

    LogLevel level = LogLevel.valueOf(newLevel.toUpperCase());

    loggingSystem.setLogLevel(loggerName, level);
    log.info("更新紀錄檔級別:{} 更新完畢", newLevel);
   
}

方案二、紀錄檔框架提供的API

參考美團技術文章:https://tech.meituan.com/2017/02/17/change-log-level.html

想必現在的業務系統基本都是採用SLF4J紀錄檔框架吧,在應用初始化時,SLF4J會繫結具體的紀錄檔框架,如Log4j、Logback或Log4j2等。具體原始碼如下(slf4j-api-1.7.7):

private final static void bind() {
  try {
    // 查詢classpath下所有的StaticLoggerBinder類。
    Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet(); 
    reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
    // 每一個slf4j橋接包中都有一個org.slf4j.impl.StaticLoggerBinder類,該類實現了LoggerFactoryBinder介面。
    // the next line does the binding
    StaticLoggerBinder.getSingleton();
    INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
    reportActualBinding(staticLoggerBinderPathSet);
    fixSubstitutedLoggers();
    ...
}

findPossibleStaticLoggerBinderPathSet方法用來查詢當前classpath下所有的org.slf4j.impl.StaticLoggerBinder類。每一個slf4j橋接包中都有一個StaticLoggerBinder類,該類實現了LoggerFactoryBinder介面。具體系結到哪一個紀錄檔框架則取決於類載入順序。

動態調整紀錄檔級別具體實現步驟如下:

2.1 初始化

確定所使用的紀錄檔框架,獲取組態檔中所有的Logger記憶體範例,並將它們的參照快取到Map容器中。

String type = StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr();
if (LogConstant.LOG4J_LOGGER_FACTORY.equals(type)) {
    logFrameworkType = LogFrameworkType.LOG4J;
    Enumeration enumeration = org.apache.log4j.LogManager.getCurrentLoggers();
    while (enumeration.hasMoreElements()) {
        org.apache.log4j.Logger logger = (org.apache.log4j.Logger) enumeration.nextElement();
        if (logger.getLevel() != null) {
            loggerMap.put(logger.getName(), logger);
        }
    }
    org.apache.log4j.Logger rootLogger = org.apache.log4j.LogManager.getRootLogger();
    loggerMap.put(rootLogger.getName(), rootLogger);
} else if (LogConstant.LOGBACK_LOGGER_FACTORY.equals(type)) {
    logFrameworkType = LogFrameworkType.LOGBACK;
    ch.qos.logback.classic.LoggerContext loggerContext = (ch.qos.logback.classic.LoggerContext) LoggerFactory.getILoggerFactory();
    for (ch.qos.logback.classic.Logger logger : loggerContext.getLoggerList()) {
        if (logger.getLevel() != null) {
            loggerMap.put(logger.getName(), logger);
        }
    }
    ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
    loggerMap.put(rootLogger.getName(), rootLogger);
} else if (LogConstant.LOG4J2_LOGGER_FACTORY.equals(type)) {
    logFrameworkType = LogFrameworkType.LOG4J2;
    org.apache.logging.log4j.core.LoggerContext loggerContext = (org.apache.logging.log4j.core.LoggerContext) org.apache.logging.log4j.LogManager.getContext(false);
    Map<String, org.apache.logging.log4j.core.config.LoggerConfig> map = loggerContext.getConfiguration().getLoggers();
    for (org.apache.logging.log4j.core.config.LoggerConfig loggerConfig : map.values()) {
        String key = loggerConfig.getName();
        if (StringUtils.isBlank(key)) {
            key = "root";
        }
        loggerMap.put(key, loggerConfig);
    }
} else {
    logFrameworkType = LogFrameworkType.UNKNOWN;
    LOG.error("Log框架無法識別: type={}", type);
}

2.2 獲取Logger列表

從本地Map容器取出,封裝成包含loggerName、logLevel的物件。

private String getLoggerList() {
    JSONObject result = new JSONObject();
    result.put("logFramework", logFrameworkType);
    JSONArray loggerList = new JSONArray();
    for (ConcurrentMap.Entry<String, Object> entry : loggerMap.entrySet()) {
        JSONObject loggerJSON = new JSONObject();
        loggerJSON.put("loggerName", entry.getKey());
        if (logFrameworkType == LogFrameworkType.LOG4J) {
            org.apache.log4j.Logger targetLogger = (org.apache.log4j.Logger) entry.getValue();
            loggerJSON.put("logLevel", targetLogger.getLevel().toString());
        } else if (logFrameworkType == LogFrameworkType.LOGBACK) {
            ch.qos.logback.classic.Logger targetLogger = (ch.qos.logback.classic.Logger) entry.getValue();
            loggerJSON.put("logLevel", targetLogger.getLevel().toString());
        } else if (logFrameworkType == LogFrameworkType.LOG4J2) {
            org.apache.logging.log4j.core.config.LoggerConfig targetLogger = (org.apache.logging.log4j.core.config.LoggerConfig) entry.getValue();
            loggerJSON.put("logLevel", targetLogger.getLevel().toString());
        } else {
            loggerJSON.put("logLevel", "Logger的型別未知,無法處理!");
        }
        loggerList.add(loggerJSON);
    }
    result.put("loggerList", loggerList);
    LOG.info("getLoggerList: result={}", result.toString());
    return result.toString();
}

結果:

{
    "loggerList": [
        {
            "logLevel": "OFF",
            "loggerName": "org.springframework.ldap"
        },
        {
            "logLevel": "INFO",
            "loggerName": "ROOT"
        },
        {
            "logLevel": "OFF",
            "loggerName": "com.sun.jersey.api.client"
        },
        {
            "logLevel": "OFF",
            "loggerName": "com.netflix.discovery"
        }
    ],
    "logFramework": "LOGBACK"
}

2.3 修改紀錄檔級別

通過呼叫具體的紀錄檔框架提供的API setLevel修改Logger紀錄檔級別,修改成功,立即生效。

private String setLogLevel(JSONArray data) {
    LOG.info("setLogLevel: data={}", data);
    List<LoggerBean> loggerList = parseJsonData(data);
    if (CollectionUtils.isEmpty(loggerList)) {
        return "";
    }
    for (LoggerBean loggerbean : loggerList) {
        Object logger = loggerMap.get(loggerbean.getName());
        if (logger == null) {
            throw new RuntimeException("需要修改紀錄檔級別的Logger不存在");
        }
        if (logFrameworkType == LogFrameworkType.LOG4J) {
            org.apache.log4j.Logger targetLogger = (org.apache.log4j.Logger) logger;
            org.apache.log4j.Level targetLevel = org.apache.log4j.Level.toLevel(loggerbean.getLevel());
            targetLogger.setLevel(targetLevel);
        } else if (logFrameworkType == LogFrameworkType.LOGBACK) {
            ch.qos.logback.classic.Logger targetLogger = (ch.qos.logback.classic.Logger) logger;
            ch.qos.logback.classic.Level targetLevel = ch.qos.logback.classic.Level.toLevel(loggerbean.getLevel());
            targetLogger.setLevel(targetLevel);
        } else if (logFrameworkType == LogFrameworkType.LOG4J2) {
            org.apache.logging.log4j.core.config.LoggerConfig loggerConfig = (org.apache.logging.log4j.core.config.LoggerConfig) logger;
            org.apache.logging.log4j.Level targetLevel = org.apache.logging.log4j.Level.toLevel(loggerbean.getLevel());
            loggerConfig.setLevel(targetLevel);
            org.apache.logging.log4j.core.LoggerContext ctx = (org.apache.logging.log4j.core.LoggerContext) org.apache.logging.log4j.LogManager.getContext(false);
            ctx.updateLoggers(); // This causes all Loggers to refetch information from their LoggerConfig.
        } else {
            throw new RuntimeException("Logger的型別未知,無法處理!");
        }
    }
    return "success";
}

方案三、spring-boot-starter-actuator

3.1 引入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

3.2 開啟紀錄檔端點設定

# 由於Spring Boot 2.x預設只暴露 /health 以及 /info 端點,
# 而紀錄檔控制需要用到 /loggers 端點,故而需要設定將其暴露。當然把loggers替換成*也是可以的;開啟所有!
management:
  endpoints:
    web:
      exposure:
        include: 'loggers'

可以通過存取URL/actuator/loggers/後加包名或者類名來查詢指定包或者類的當前紀錄檔級別。

curl http://127.0.0.1:8007/manage/actuator/loggers/com.trrt.ep
{"configuredLevel":"DEBUG","effectiveLevel":"DEBUG"}

3.3 檢視所有Logger

http://127.0.0.1:8007/manage/actuator/loggers

3.4 修改紀錄檔級別

可以通過存取URL/actuator/loggers/後加包名或者類名來修改指定包或者類的當前紀錄檔級別。

curl -X POST "http://127.0.0.1:8007/manage/actuator/loggers/com.trrt.ep" -H "Content-Type: application/json;charset=UTF-8" --data '{"configuredLevel":"debug"}'

最後,如果你覺得這篇文章有用,動動你的小手點個贊吧