• 实战:如何优雅地扩展Log4j配置?


    前言

    Log4j 日志框架我们经常会使用到,最近,我就遇到了一个与日志配置相关的问题。简单来说,就是在原来日志配置的基础上,指定类的日志打印到指定的日志文件中。

    这样讲述可能不是那么好理解,且听我从需求来源讲起。

    一、扩展配置的需求来源

    我们的项目中使用的是 Log4j2 日志框架,日志配置log4j.yml是这样的:

     Configuration:
       status: warn
       
       Appenders:
         Console:
           name: Console
           target: SYSTEM_OUT
           # 不重要
         RollingFile:
           - name: ROLLING_FILE
             # 不重要
       Loggers:
         Root:
           level: info
           AppenderRef:
             - ref: Console
             - ref: ROLLING_FILE
         Logger:
           - name: com.myproject
             level: info
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    配置很简单,只是一个滚动日志文件和控制台的输出。现在来了这么一个需求:要把项目的 HTTP 接口访问日志单独打印到一个日志文件logs/access.log中,这个功能由配置开关casslog.accessLogEnabled决定是否开启。

    说做就做,我立马把原来的log4j.yml文件改成log4j_with_accesslog.yml,并添加了访问日志的AppenderACCESS_LOG,如下配置所示。

     Configuration:
       status: warn
       
       Appenders:
         Console:
           name: Console
           target: SYSTEM_OUT
           # 不重要
         RollingFile:
           - name: ROLLING_FILE
             # 不重要
             ### 新增的配置开始(1) ###
           - name: ACCESS_LOG
             fileName: logs/access.log
             ### 新增的配置结束(1) ###
       Loggers:
         Root:
           level: info
           AppenderRef:
             - ref: Console
             - ref: ROLLING_FILE
         Logger:
           - name: com.myproject
             level: info
           ### 新增的配置开始(2) ###
           - name: com.myproject.commons.AccessLog
             level: trace
             additivity: false
             AppenderRef:
               - ref: Console
               - ref: ACCESS_LOG
           ### 新增的配置结束(2) ###
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    上面配置注释中【新增的配置开始(1)】和【新增的配置开始(2)】就是添加的配置内容。功能开关是下面这样实现的,在项目启动时做判断。

     import org.springframework.boot.logging.log4j2.Log4J2LoggingSystem;
     ​
     public class MyProjectLoggingSystem extends Log4J2LoggingSystem {
     ​
         static final boolean accessLogEnabled =
                 Boolean.parseBoolean(System.getProperty("casslog.accessLogEnabled", "true"));
     ​
         @Override
         protected String[] getStandardConfigLocations() {
             if (accessLogEnabled) {
                 return new String[]{"casslog_with_accesslog.yml"};
             }
             return new String[]{"casslog.yml"};
         }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这样功能就实现了,程序也确实可以运行。但是总感觉不够优雅,如果有上百个项目都要加上这个功能,这些项目的日志配置文件都要改,想想都崩溃。

    二、看看开源项目 Nacos 的实现

    使用过 Nacos 的朋友可能知道,Nacos 的配置模块与服务发现模块是两个功能,日志也是分开的。具体通过nacos-client.jar中的nacos-log4j2.xml就可以看出来。

    image-20221118105141675

    注意本文 Nacos 源码版本是nacos-client 1.4.1

    nacos-log4j2.xml我做了精简,内容如下。

     
         
             
                 
             
             
                 
             
         
         
         
             
             
                 
             
             
                 
             
             
         
     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    通过以上日志配置可以看到,Nacos 将包名为com.alibaba.nacos.client.config的类的日志输出到${sys:JM.LOG.PATH}/nacos/config.log文件中,将包名为com.alibaba.nacos.client.naming的类的日志输出到${sys:JM.LOG.PATH}/nacos/naming.log文件中。${sys:JM.LOG.PATH}默认配置的路径就是用户目录。

    接下来,我们看看 Nacos 是如何将日志配置加载进应用程序的。(实现代码请自行赏析)

     import static org.slf4j.LoggerFactory.getLogger;
     ​
     public class LogUtils {
         public static final Logger NAMING_LOGGER;
         static {
             NacosLogging.getInstance().loadConfiguration();
             NAMING_LOGGER = getLogger("com.alibaba.nacos.client.naming");
         }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
     public class NacosLogging {
         private AbstractNacosLogging nacosLogging;
         public void loadConfiguration() {
             try {
                 nacosLogging.loadConfiguration();
             }
             // 省略...
         }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
     public abstract class AbstractNacosLogging {
         public abstract void loadConfiguration();
     }
    
    • 1
    • 2
    • 3
     public class Log4J2NacosLogging extends AbstractNacosLogging {
         private final String location = getLocation("classpath:nacos-log4j2.xml");
         @Override
         public void loadConfiguration() {
             final LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
             final Configuration contextConfiguration = loggerContext.getConfiguration();
             
             // load and start nacos configuration
             Configuration configuration = loadConfiguration(loggerContext, location);
             configuration.start();
             
             // append loggers and appenders to contextConfiguration
             Map appenders = configuration.getAppenders();
             for (Appender appender : appenders.values()) {
                 contextConfiguration.addAppender(appender);
             }
             Map loggers = configuration.getLoggers();
             for (String name : loggers.keySet()) {
                 if (name.startsWith(NACOS_LOGGER_PREFIX)) {
                     contextConfiguration.addLogger(name, loggers.get(name));
                 }
             }
             
             loggerContext.updateLoggers();
         }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    总结来说,就是先将扩展配置(即nacos-log4j2.xml)转化成LoggerConfig对象;然后将LoggerConfig实例添加到应用的日志配置上下文contextConfiguration中;最后更新应用的Loggers

    三、即学即用

    我们就把扩展日志当成一个对象,比如这里的「访问日志」,Nacos 中的「配置模块日志」都可以称为扩展日志。我们先来编写扩展日志的抽象AbstractLogExtend

     @Slf4j
     public abstract class AbstractLogExtend {
         public void loadConfiguration() {
             final LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
             final Configuration contextConfiguration = loggerContext.getConfiguration();
     ​
             // load and start casslog extend configuration
             Configuration configurationExtend = loadConfiguration(loggerContext);
             configurationExtend.start();
     ​
             // append loggers and appenders to contextConfiguration
             Map appenders = configurationExtend.getAppenders();
             for (Appender appender : appenders.values()) {
                 addAppender(contextConfiguration, appender);
             }
             Map loggersExtend = configurationExtend.getLoggers();
             loggersExtend.forEach((loggerName, loggerConfig) ->
                     addLogger(contextConfiguration, loggerName, loggerConfig)
             );
     ​
             loggerContext.updateLoggers();
         }
         private Configuration loadConfiguration(LoggerContext loggerContext) {
             try {
                 URL url = ResourceUtils.getResourceUrl(logConfig());
                 ConfigurationSource source = getConfigurationSource(url);
                 // since log4j 2.7 getConfiguration(LoggerContext loggerContext, ConfigurationSource source)
                 return ConfigurationFactory.getInstance().getConfiguration(loggerContext, source);
             } catch (Exception e) {
                 throw new IllegalStateException("Could not initialize Log4J2 logging from " + logConfig(), e);
             }
         }
         /**
          * 要扩展配置的文件名
          */
         public abstract String logConfig();
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    AbstractLogExtend定义了两个方法,分别是:

    • loadConfiguration():加载扩展日志配置;
    • logConfig():扩展日志配置文件的路径;

    然后我们把这些扩展日志加载进应用中。

     public class LogExtendInitializer {
         
         private final List cassLogExtends;
         
         @PostConstruct
         public void init() {
             cassLogExtends.forEach(cassLogExtend -> {
                 try {
                     cassLogExtend.loadConfiguration();
                 }
                 // 省略...
             });
         }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    到这里,基础类代码写好了。下面我们回到文章开头的需求,来看看如何实现。

    首先配置访问日志accesslog-log4j.xml

     
         
             
             
                 
             
         
             
             
                 
                 
             
         
     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    我这里将accesslog-log4j.xml放在了类包下。

    image-20221118111846952

    接着就是配置accesslog-log4j.xml的文件的路径,这里我把「访问日志」定义成了对象AccessLogConfigExtend

     public class AccessLogConfigExtend extends AbstractLogExtend {
     ​
         @Override
         public String logConfig() {
             return "classpath:com/github/open/casslog/accesslog/accesslog-log4j.xml";
         }
     ​
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这样访问日志就配置好了,也可以将访问日志封装成基础jar包供其他项目使用,这样其他项目就不需要重复配置了。

    对于配置开关,可以使用@Conditional来实现,具体如下。

     @Configuration
     @ConditionalOnProperty(value = "casslog.accessLogEnabled")
     public class AccessLogAutoConfiguration {
     ​
         @Bean
         public AccessLogConfigExtend accessLogConfigExtend() {
             return new AccessLogConfigExtend();
         }
     ​
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这样实现,确实优雅了很多!

    小结

    本案例是我之前在做日志组件实现的一个功能,源码放在了我的 Github 上:https://github.com/studeyang/casslog。一开始实现访问日志,就是通过文章中所说的不优雅的方式实现的,后来在做消息消费的监控时,想把消费的日志单独放到新日志文件中,供 ELK 采集分析。于是提取【访问日志】与【消费监控】的功能共性,实现日志的扩展。

    如果你想和我交流,欢迎关注我的微信公众号【杨同学technotes】。

  • 相关阅读:
    Java 网络编程
    一同走进Linux的“基操”世界
    软件开发工具的现状与发展
    了解Docker 依赖的linux内核技术
    本周内容整理
    数据库 高阶语句
    浅谈实时计算
    Qt——(详细)“项目在Debug构建环境下能运行而在Release构建环境下不能运行”解决方案之一,以及 禁用(黄色)警告
    5+甲基化+预后模型搭配实验
    leetcode刷题(125)——931. 下降路径最小和
  • 原文地址:https://blog.csdn.net/yang237061644/article/details/127958832