As developers, it is our daily job to locate problems, and logs are a very important basis for us to locate problems. Traditionally, the following steps are used to locate problems:
- Set the log level to low, for example, DEBUG.
- Restart the application.
- Reappear the problem, observe the log;
In fact, the log level can be dynamically changed without restarting the application. This article has collected three kinds of dynamic log level change articles, respectively
- Spring Boot 2 Dynamically changes the log level
- Arthas, Alibaba’s online diagnostic tool, adjusts log level recording
- Meituan log level dynamic adjustment – small tools to solve big problems
Spirng Boot Dynamically changes the log level
Since Spring Boot 1.5, Spring Boot Actuator components provide the ability to dynamically change the log level.
The sample
1. Use spring-boot-starter-actuator dependencies as follows:
<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>
Copy the code
2. Write the test code as follows:
@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(a){
logger.debug("welcome to learn spring boot");
return "welcome to learn spring boot"; }}Copy the code
3. Configuration file
management:
endpoints:
web:
exposure:
include: 'loggers'
Copy the code
Spring Boot 2.x exposes only /health and /info endpoints by default, and /loggers endpoints are needed for logging control, so you need to set them to be exposed.
test
The /loggers endpoint provides the ability to view and modify log levels.
-
View the current application of each package/class log level to http://localhost:8080/actuator/loggers, you can see the similar to the following results:
-
Check the specified package/class log details to http://localhost:8080/actuator/loggers/com.blockmao.springboot.demo.DemoController, you can see the similar to the following results:
-
Changing the Log Level The default log level is INFO. Therefore, the debug logs of DemoController are not displayed. Let’s try to set the log level of this class to DEBUG as follows
At this point, the log to http://localhost:8080/helloworld will see similar to the following:
And, at this time to visit http://localhost:8080/actuator/loggers/com.itmuch.logging.TestController again, you can see the similar to the following results:
The principle of
The code of/Actuator/XXX endpoint is defined in xxxEndpoint. Find the class org. Springframework. Boot. Actuate. Logging. LoggersEndpoint, code is as follows:
@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);
}
/ /... Other omitted
}
Copy the code
Endpoint, WriteOperation, and @Selector are all new annotations provided by Spring Boot 2.0.
@endpoint (id = “loggers”) is used to describe the endpoints of Spring Boot actuators. This creates a/Actuator /loggers path. It is similar to @RequestMapping(“loggers”) for Spring MVC.
@writeoperation indicates that this is a WriteOperation, which is similar to @postmapping in Spring MVC. Spring Boot Actuator also provides other operations, as shown in the following table:
Operation | HTTP method |
---|---|
@ReadOperation | GET |
@WriteOperation | POST |
@DeleteOperation | DELETE |
@Selector is used to filter a subset of the values returned by the @endpoint annotation, which is similar to @pathVariable in Spring MVC.
The configureLogLevel method: After the POST request is sent, name is the package name or class name, and configuredLevel is the message body.
Org. Springframework. Boot. Logging. LoggingSystem# setLogLevel is abstract method, concrete implementation performed by subclasses. The LoggingSystem class structure is shown below:
With so many LoggingSystem implementation classes, how does Spring Boot know which LoggingSystem to use in which situations? In the org. Springframework. Boot. Logging. LoggingSystem find similar to the following code:
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"));
}
// omit irrelevant content...
}
Copy the code
You can see from the code that you have built a map named SYSTEMS that acts as a dictionary for various logging SYSTEMS. Then, in the get method, see if the application loads the classes in the map; If loaded, LoggingSystem is initialized via reflection. Such as: Spring Boot found the current application loaded the ch. The qos. Logback. Core. The Appender, went to instantiate the org. Springframework. Boot. Logging. Logback. LogbackLoggingSystem.
The Arthas ognl command dynamically changes the log level
To dynamically change the log level, run the ognl command as follows:
-
Finds the classLoaderHash of the current class
-
Get the Logger with OGNL
You can see that logs use the Logback framework.
-
Set the Logger level of the DemoController separately
-
Set logger level globally
If the logging framework used is Log4J, an error is reported using the ognl command. And why? Read Java Logging: SLF4J for details
Meituan log level dynamic adjustment tool
-
Initialization: Determine the logging framework used, get all Logger memory instances in the configuration file, and cache their references to the Map container.
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 frame cannot recognize: type={}", type); } Copy the code
-
Get Logger list: from local Map container.
private String getLoggerList(a) { 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 type unknown, unable to handle!"); } loggerList.add(loggerJSON); } result.put("loggerList", loggerList); LOG.info("getLoggerList: result={}", result.toString()); return result.toString(); } Copy the code
-
Change the Logger level
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 whose log level needs to be changed does not exist"); } 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 type unknown, unable to handle!"); }}return "success"; } Copy the code