8

动态修改日志级别,太有用了! - 问北

 1 year ago
source link: https://www.cnblogs.com/ibigboy/p/17656193.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.



我们在系统中一般都会打印一些日志,并且在开发、测试、生产各个环境中的日志级别可能不一样。在开发过程中为了方便调试打印了很多debug日志,但是生产环境为了性能,为了节约存储资源,我们会将日志级别设置为info或error较高的级别,只保留一些关键的必要的日志。

当线上出现问题需要排查时,最有效的方式是分析系统日志。此时因为线上环境日志级别较高,对排查问题有一定的阻碍,为了快速响应线上问题,我们需要更全面的日志帮助排查问题,传统的做法是修改日志级别重启项目。

为了兼顾性能和快速响应线上问题,实现不重启项目的前提下动态修改日志级别。通过使用该功能,可以在需要解决线上问题时,实时调整线上日志输出级别,获取全面的Debug日志,帮助工程师提高定位问题的效率。

本文列举了几种实现方案,已经验证可用,供大家参考。

方案一、LoggingSystem

在Spring Boot项目中可以通过LoggingSystem来获取或修改日志配置。

1.1 获取日志Logger配置

通过LoggingSystem API getLoggerConfigurations获取所有Logger配置

List loggerConfigs = loggingSystem.getLoggerConfigurations();

1205690-20230825100545014-1377743893.png

1.2 修改日志级别

通过调用LoggingSystem API setLogLevel设置包或具体Logger的日志级别,修改成功,立即生效。

@Autowiredprivate 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

1205690-20230825101124465-227017663.png

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

最后,如果你觉得这篇文章有用,动动你的小手点个赞吧


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK