一、背景
项目中使用日志的地方非常广泛, 如果要做统一的处理,比如调整格式, 增加信息打印, 要修改很多地方,难以维护,所以需要能够将日志做一个统一的封装, 便于扩展使用;同时想做到动态调整日志级别,在生产环境修改日志级别, 将开销降到最低, 在出现故障时, 又可以重新调整日志级别而不需要重新启动服务, 便于问题的排查定位。
二、统一日志的封装
如果实现统一日志, 需要自己封装一层实现,但在实际日志打印中,往往会打印出所封装的类信息, 而并非实际调用的类信息, 该如何解决这个问题? 网上有很资料, 一般都是采用AOP+反射方式来修改类信息,这样虽能实现,但性能并不理想,这里我们采用最简单的方式直接封装slf4j中的Logger,这样可以支持Log4j和Logback。
-
先定义日志代理类
LogProxy:
import org.slf4j.Logger; import org.slf4j.Marker; ... public class LogProxy implements Logger { private Logger log; /** * 构造方法,注入类信息 */ public LogProxy(Logger logger) { log = logger; } /*** * debug打印信息 * @param msg */ public void debug(String msg) { log.debug(msg); } ... }
这里通过构造方法 ,将原始Logger注入进来。
-
定义日志工厂
LogFactory
import org.slf4j.Logger; import org.slf4j.LoggerFactory; ... public class LogFactory { /** * 创建封装的日志代理 * @param clazz * @return */ public static LogProxy getLogProxy(Class clazz) { Logger logger = LoggerFactory.getLogger(clazz); return new LogProxy(logger); } }
-
基准测试验证
通过JMH做并发性能基准测试, 测试代码:
@BenchmarkMode(Mode.AverageTime) // 指定mode为Mode.AverageTime @Benchmark @Fork(1) @Threads(5) @OutputTimeUnit(TimeUnit.MILLISECONDS) // 指定输出的耗时时长的单位 @Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) public void testLog() { ClassLog1Test c1 = new ClassLog1Test(); c1.test(); ClassLog2Test c2 = new ClassLog2Test(); c2.test(); }
开启5个线程进行压测, 内部创建2个实现类,打印不同的日志信息,测试结果:
可以看到,日志信息是能够正确有效的输出,能打印出具体每个调用类的信息。
查看性能基准测试结果:
最终测试结果平均响应耗时在0.315ms,偏差0.174毫秒,整体性能也是比较高的。
三、基于Apollo的动态日志级别调整
基于Apollo配置中心, 实现对日志级别的动态调整,创建一个监听器, 接收Apollo变更的配置信息。
-
监听器
LoggingAdjustListener:
@Component public class LoggingAdjustListener { private final static LogProxy log = LogFactory.getLogProxy(LoggingAdjustListener.class); /** * 日志配置项的前缀 */ private static final String LOGGER_PREFIX = "logging.level."; @Resource private LoggingSystem loggingSystem; /** * Apollo事件监听处理 * @param changeEvent * @throws Exception */ @ApolloConfigChangeListener public void onChange(ConfigChangeEvent changeEvent) throws Exception { // <Y> 遍历配置集的每个配置项,判断是否是 logging.level 配置项 for (String key : changeEvent.changedKeys()) { // 如果是 logging.level 配置项,则设置其对应的日志级别 if (key.startsWith(LOGGER_PREFIX)) { String loggerName = key.replace(LOGGER_PREFIX, ""); LoggerConfiguration cfg = loggingSystem.getLoggerConfiguration(loggerName); if (cfg == null) { log.error("no loggerConfiguration with loggerName:{}", loggerName); continue; } // 获得日志级别 ConfigChange change = changeEvent.getChange(key); String newLevel = change.getNewValue(); if(null == newLevel) { log.error("loggerConfiguration have no value:{}", loggerName); continue; } LogLevel level = LogLevel.valueOf(newLevel.toUpperCase()); if (level == null) { log.error("logger:[{}] current LogLevel is invalid:{}", loggerName, newLevel); continue; } if (!isSupportLevel(level)) { log.error("not support LogLevel:{}", newLevel); continue; } log.info("logger:[{}] current level:{}, changed to level:{}", loggerName, cfg.getEffectiveLevel(), newLevel); // 设置日志级别到 LoggingSystem 中 loggingSystem.setLogLevel(loggerName, level); } } } /** * 判断是否支持的日志级别 * @param level * @return */ private boolean isSupportLevel(LogLevel level) { for (LogLevel logLevel : loggingSystem.getSupportedLogLevels()) { if (logLevel == level) { return true; } } return false; } }
这个监听器要注入Spring容器中,并且加上@ApolloConfigChangeListener注解才能生效,接收到时间消息后,会做校验判断, 然后通过loggingSystem重新设置日志级别,该接口是由Spring Boot提供,支持动态修改日志级别。
-
在Apollo中调整日志级别
将com.mirson.log包的日志级别设为info, 查看控制台的日志打印:
可以看到监听器能够正常接收Apollo推送的事件信息, 日志级别由warn调整到了info。 -
通过接口进行测试验证
TestController:
@RestController public class TestController { ... @RequestMapping(value = "/test") @ResponseBody public String test() { log.debug("this is a debug"); log.info("this is a info"); log.warn("this is a warn"); log.error("this is a error"); return "execute "; } }
查看控制台的日志输出,只输出了info以上级别的日志: