Java日誌:日誌級別動態調整

做爲開發人員,定位問題是咱們的平常工做,而日誌是咱們定位問題很是重要的依據。傳統方式定位問題時,每每是以下步驟:html

  1. 將日誌級別設低,例如 DEBUG ;
  2. 重啓應用;
  3. 復現問題,觀察日誌;

其實是能夠動態修改日誌級別,無需重啓應用,當即生效。本文收集了3種動態修改日誌級別的文章,分別是java

  1. Spring Boot 2動態修改日誌級別
  2. 阿里在線診斷工具Arthas調整日誌等級記錄
  3. 美團日誌級別動態調整——小工具解決大問題

Spirng Boot動態修改日誌級別

從 Spring Boot 1.5 開始,Spring Boot Actuator 組件就已提供動態修改日誌級別的能力。web

示例

1.引入spring-boot-starter-actuator依賴,內容以下:spring

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
複製代碼

2.編寫測試代碼,以下:apache

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

@RestController
public class DemoController {

    private static Logger logger = LoggerFactory.getLogger(DemoController.class);

    @GetMapping("/helloworld")
    public String helloworld(){
        logger.debug("welcome to learn spring boot");
        return "welcome to learn spring boot";
    }

}
複製代碼

3.配置文件緩存

management:
 endpoints:
 web:
 exposure:
 include: 'loggers'
複製代碼

Spring Boot 2.x默認只暴露 /health 以及 /info 端點,而日誌控制須要用到 /loggers 端點,故而須要設置將其暴露。springboot

測試

/loggers端點提供了查看以及修改日誌級別的能力。app

  1. 查看當前應用各包/類的日誌級別 訪問 http://localhost:8080/actuator/loggers ,可看到相似以下的結果:框架

  2. 查看指定包/類日誌詳情 訪問 http://localhost:8080/actuator/loggers/com.blockmao.springboot.demo.DemoController ,可看到相似以下的結果:spring-boot

  3. 修改日誌級別 默認的日誌級別是INFO,因此DemoController的debug日誌不會打印。下面來嘗試將該類的日誌級別設爲DEBUG,以下

    此時,訪問 http://localhost:8080/helloworld 會看到相似以下的日誌:

    而且,此時再訪問 http://localhost:8080/actuator/loggers/com.itmuch.logging.TestController ,可看到相似以下的結果:

原理

Actuator有約定,/actuator/xxx端點的定義代碼在 xxxEndpoint中。找到類org.springframework.boot.actuate.logging.LoggersEndpoint,代碼以下:

@Endpoint(id = "loggers")
public class LoggersEndpoint {
	private final LoggingSystem loggingSystem;

	@WriteOperation
	public void configureLogLevel(@Selector String name, @Nullable LogLevel configuredLevel) {
		Assert.notNull(name, "Name must not be empty");
		this.loggingSystem.setLogLevel(name, configuredLevel);
	}
	// ...其餘省略
}
複製代碼

其中, EndpointWriteOperation@Selector都是Spring Boot 2.0開始提供的新註解。

@Endpoint(id = "loggers")用來描述Spring Boot Actuator 的端點,這樣就會產生一個/actuator/loggers的路徑,它相似於Spring MVC的@RequestMapping("loggers")

@WriteOperation 表示這是一個寫操做,它相似於Spring MVC的 @PostMapping 。Spring Boot Actuator還提供了其餘操做,以下表:

Operation HTTP method
@ReadOperation GET
@WriteOperation POST
@DeleteOperation DELETE

@Selector用於篩選@Endpoint註解返回值的子集,它相似於Spring MVC的@PathVariable 。

這樣,上面的代碼就很好理解了— configureLogLevel 方法:送POST請求後,name就是咱們傳的包名或者類名,configuredLevel就是咱們傳的消息體。

org.springframework.boot.logging.LoggingSystem#setLogLevel是抽象方法,具體實現由子類完成。LoggingSystem類結構以下圖所示:

LoggingSystem有這麼多實現類,Spring Boot怎麼知道什麼狀況下用什麼LoggingSystem呢?可在 org.springframework.boot.logging.LoggingSystem 找到相似以下代碼:

public abstract class LoggingSystem {
	private static final Map<String, String> SYSTEMS;

	static {
		Map<String, String> systems = new LinkedHashMap<>();
		systems.put("ch.qos.logback.core.Appender",
				"org.springframework.boot.logging.logback.LogbackLoggingSystem");
		systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory",
				"org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
		systems.put("java.util.logging.LogManager",
				"org.springframework.boot.logging.java.JavaLoggingSystem");
		SYSTEMS = Collections.unmodifiableMap(systems);
	}

	/** * Detect and return the logging system in use. Supports Logback and Java Logging. * @param classLoader the classloader * @return the logging system */
	public static LoggingSystem get(ClassLoader classLoader) {
		String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
		if (StringUtils.hasLength(loggingSystem)) {
			if (NONE.equals(loggingSystem)) {
				return new NoOpLoggingSystem();
			}
			return get(classLoader, loggingSystem);
		}
		return SYSTEMS.entrySet().stream()
				.filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader))
				.map((entry) -> get(classLoader, entry.getValue())).findFirst()
				.orElseThrow(() -> new IllegalStateException(
						"No suitable logging system located"));
	}
  // 省略不相關內容...
}
複製代碼

由代碼不難發現,其實就是構建了一個名爲 SYSTEMS 的map,做爲各類日誌系統的字典;而後在 get 方法中,看應用是否加載了map中的類;若是加載了,就經過反射,初始化LoggingSystem 。例如:Spring Boot發現當前應用加載了ch.qos.logback.core.Appender ,就去實例化 org.springframework.boot.logging.logback.LogbackLoggingSystem

Arthas ognl命令動態修改日誌級別

使用ognl命令能夠動態修改日誌級別,步驟以下:

  1. 查找當前類的classLoaderHash

  2. 用OGNL獲取logger

    能夠發現日誌使用的是Logback框架。

  3. 單獨設置DemoController的logger level

  4. 全局設置logger level

若是使用的日誌框架是log4j,則使用上述ognl命令則會報錯。至於爲何?請閱讀Java日誌:SLF4J詳解

美團日誌級別動態調整小工具

  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. 獲取Logger列表:從本地Map容器取出。

    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();
    }
    複製代碼
  3. 修改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";
    }
    複製代碼
相關文章
相關標籤/搜索