动态修改日志级别,太有用了!
背景
我们在系统中一般都会打印一些日志,并且在开发、测试、生产各个环境中的日志级别可能不一样。在开发过程中为了方便调试打印了很多debug日志,但是生产环境为了性能,为了节约存储资源,我们会将日志级别设置为info或error较高的级别,只保留一些关键的必要的日志。
当线上出现问题需要排查时,最有效的方式是分析系统日志。此时因为线上环境日志级别较高,对排查问题有一定的阻碍,为了快速响应线上问题,我们需要更全面的日志帮助排查问题,传统的做法是修改日志级别重启项目。
目标
为了兼顾性能和快速响应线上问题,实现不重启项目的前提下动态修改日志级别。通过使用该功能,可以在需要解决线上问题时,实时调整线上日志输出级别,获取全面的Debug日志,帮助工程师提高定位问题的效率。
技术方案
本文列举了几种实现方案,已经验证可用,供大家参考。
方案一、LoggingSystem
在Spring Boot项目中可以通过LoggingSystem来获取或修改日志配置。
1.1 获取日志Logger配置
通过LoggingSystem API getLoggerConfigurations获取所有Logger配置
List loggerConfigs = loggingSystem.getLoggerConfigurations();
1.2 修改日志级别
通过调用LoggingSystem API setLogLevel设置包或具体Logger的日志级别,修改成功,立即生效。
@Autowired private 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
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"}'
最后,如果你觉得这篇文章有用,动动你的小手点个赞吧
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?