Наша повседневная работа, как разработчиков, заключается в обнаружении проблем, и журналы являются для нас очень важной основой для обнаружения проблем. При локализации проблемы традиционным способом часто используются следующие шаги:
- Установите низкий уровень журнала, например DEBUG ;
- перезапустите приложение;
- Чтобы воспроизвести проблему, просмотрите журнал;
На самом деле уровень журнала можно динамически изменять без перезапуска приложения, и это вступит в силу немедленно. В этой статье собраны три вида статей, которые динамически изменяют уровень журнала:
- Spring Boot 2 динамически изменяет уровень журнала
- Онлайн-инструмент диагностики Ali Arthas корректирует записи на уровне журнала
- Динамическая настройка уровня журнала 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Конечные точки предоставляют возможность просматривать и изменять уровни журналов.
-
Просмотр уровня журнала каждого пакета/класса текущего приложения доступhttp://localhost:8080/actuator/loggers, вы можете увидеть результаты, подобные следующим:
-
Просмотр сведений о конкретном пакете/классе в журнале доступhttp://localhost:8080/actuator/loggers/com.blockmao.springboot.demo.DemoController, вы можете увидеть результаты, подобные следующим:
-
Изменить уровень журнала Уровень журнала по умолчанию — 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Команда может динамически изменять уровень журнала, шаги следующие:
-
Найдите classLoaderHash текущего класса
-
Получить регистратор с OGNL
Можно обнаружить, что журнал использует структуру Logback.
-
Установите уровень регистратора DemoController отдельно
-
Установить глобальный уровень ведения журнала
Если используемая структура ведения журнала
log4j, затем используйте приведенное вышеognlКоманда сообщит об ошибке. А почему? пожалуйста прочтиЖурнал Java: объяснение SLF4J
Виджет динамической настройки уровня журнала Meituan
-
Инициализация: определите используемую структуру ведения журналов, получите все экземпляры памяти 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); } -
Получить список регистраторов: из локального контейнера карты.
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(); } -
Изменить уровень регистратора
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"; }