Ведение журнала Java: динамическая настройка уровней журнала

Java

Наша повседневная работа, как разработчиков, заключается в обнаружении проблем, и журналы являются для нас очень важной основой для обнаружения проблем. При локализации проблемы традиционным способом часто используются следующие шаги:

  1. Установите низкий уровень журнала, например DEBUG ;
  2. перезапустите приложение;
  3. Чтобы воспроизвести проблему, просмотрите журнал;

На самом деле уровень журнала можно динамически изменять без перезапуска приложения, и это вступит в силу немедленно. В этой статье собраны три вида статей, которые динамически изменяют уровень журнала:

  1. Spring Boot 2 динамически изменяет уровень журнала
  2. Онлайн-инструмент диагностики Ali Arthas корректирует записи на уровне журнала
  3. Динамическая настройка уровня журнала Meituan — маленькие инструменты для решения больших проблем

Springboot динамически изменяет уровень журнала

Начиная с Spring Boot 1.5, компонент Spring Boot Actuator предоставляет возможность динамически изменять уровень журнала.

Пример

1. Представьтеspring-boot-starter-actuatorЗависимость, как показано ниже:

<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. Напишите тестовый код следующим образом:

@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, поэтому ее необходимо настроить для ее предоставления.

контрольная работа

/loggersКонечные точки предоставляют возможность просматривать и изменять уровни журналов.

  1. Просмотр уровня журнала каждого пакета/класса текущего приложения доступhttp://localhost:8080/actuator/loggers, вы можете увидеть результаты, подобные следующим:

  2. Просмотр сведений о конкретном пакете/классе в журнале доступhttp://localhost:8080/actuator/loggers/com.blockmao.springboot.demo.DemoController, вы можете увидеть результаты, подобные следующим:

  3. Изменить уровень журнала Уровень журнала по умолчанию — INFO, поэтомуDemoControllerЖурнал отладки не печатается. Давайте попробуем установить уровень журнала этого класса на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);
	}
	// ...其他省略
}

в,Endpoint,WriteOperation,@SelectorВсе это новые аннотации, предоставляемые Spring Boot 2.0.

@Endpoint(id = "loggers")Используется для описания конечной точки Spring Boot Actuator, которая будет генерировать/actuator/loggersпуть, который похож на Spring MVC@RequestMapping("loggers").

@WriteOperation указывает, что это операция записи, аналогичная @PostMapping Spring MVC. Spring Boot Actuator также предоставляет другие операции, как показано в следующей таблице:

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

@Selector используется для фильтрации подмножества возвращаемого значения аннотации @Endpoint, он похож на Spring MVC @PathVariable.

Таким образом, приведенный выше код легко понять -configureLogLevelМетод: после отправки запроса POST name — это имя пакета или имя класса, которое мы передали, а configureLevel — это тело сообщения, которое мы передали.

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 на самом деле строится как словарь различных лог-систем, затем в методе get проверяется, загружает ли приложение классы в карте, если загружается, то инициализируется через отражениеLoggingSystem. Например: Spring Boot обнаруживает, что текущее приложение загружено.ch.qos.logback.core.Appender, просто создайте экземплярorg.springframework.boot.logging.logback.LogbackLoggingSystem.

Команда Arthas ognl для динамического изменения уровня журнала

использоватьognlКоманда может динамически изменять уровень журнала, шаги следующие:

  1. Найдите classLoaderHash текущего класса

  2. Получить регистратор с OGNL

    Можно обнаружить, что журнал использует структуру Logback.

  3. Установите уровень регистратора DemoController отдельно

  4. Установить глобальный уровень ведения журнала

Если используемая структура ведения журналаlog4j, затем используйте приведенное вышеognlКоманда сообщит об ошибке. А почему? пожалуйста прочтиЖурнал Java: объяснение SLF4J

Виджет динамической настройки уровня журнала Meituan

  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. Получить список регистраторов: из локального контейнера карты.

    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. Изменить уровень регистратора

    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";
    }