前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实战:如何优雅地扩展Log4j配置?

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

作者头像
杨同学technotes
发布2022-12-01 16:04:43
5430
发布2022-12-01 16:04:43
举报
文章被收录于专栏:杨同学technotes

前言

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

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

一、扩展配置的需求来源

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

代码语言:javascript
复制
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

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

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

代码语言:javascript
复制
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)】就是添加的配置内容。功能开关是下面这样实现的,在项目启动时做判断。

代码语言:javascript
复制
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"};
    }
}

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

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

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

image-20221118105141675

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

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

代码语言:javascript
复制
<Configuration status="WARN">
    <Appenders>
        <RollingFile name="CONFIG_LOG_FILE" fileName="${sys:JM.LOG.PATH}/nacos/config.log"
            filePattern="${sys:JM.LOG.PATH}/nacos/config.log.%d{yyyy-MM-dd}.%i">
            <!-- 不重要 -->
        </RollingFile>
        <RollingFile name="NAMING_LOG_FILE" fileName="${sys:JM.LOG.PATH}/nacos/naming.log"
            filePattern="${sys:JM.LOG.PATH}/nacos/naming.log.%d{yyyy-MM-dd}.%i">
            <!-- 不重要 -->
        </RollingFile>
    </Appenders>
    
    <Loggers>
        <!-- 不重要 -->
        <Logger name="com.alibaba.nacos.client.config" level="${sys:com.alibaba.nacos.config.log.level:-info}"
            additivity="false">
            <AppenderRef ref="CONFIG_LOG_FILE"/>
        </Logger>
        <Logger name="com.alibaba.nacos.client.naming" level="${sys:com.alibaba.nacos.naming.log.level:-info}"
            additivity="false">
            <AppenderRef ref="NAMING_LOG_FILE"/>
        </Logger>
        <!-- 不重要 -->
    </Loggers>
</Configuration>

通过以上日志配置可以看到,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文件中。

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

代码语言:javascript
复制
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");
    }
}
代码语言:javascript
复制
public class NacosLogging {
    private AbstractNacosLogging nacosLogging;
    public void loadConfiguration() {
        try {
            nacosLogging.loadConfiguration();
        }
        // 省略...
    }
}
代码语言:javascript
复制
public abstract class AbstractNacosLogging {
    public abstract void loadConfiguration();
}
代码语言:javascript
复制
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<String, Appender> appenders = configuration.getAppenders();
        for (Appender appender : appenders.values()) {
            contextConfiguration.addAppender(appender);
        }
        Map<String, LoggerConfig> loggers = configuration.getLoggers();
        for (String name : loggers.keySet()) {
            if (name.startsWith(NACOS_LOGGER_PREFIX)) {
                contextConfiguration.addLogger(name, loggers.get(name));
            }
        }
        
        loggerContext.updateLoggers();
    }
}

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

三、即学即用

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

代码语言:javascript
复制
@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<String, Appender> appenders = configurationExtend.getAppenders();
        for (Appender appender : appenders.values()) {
            addAppender(contextConfiguration, appender);
        }
        Map<String, LoggerConfig> 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();
}

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

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

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

代码语言:javascript
复制
public class LogExtendInitializer {
    
    private final List<AbstractLogExtend> cassLogExtends;
    
    @PostConstruct
    public void init() {
        cassLogExtends.forEach(cassLogExtend -> {
            try {
                cassLogExtend.loadConfiguration();
            }
            // 省略...
        });
    }
}

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

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

代码语言:javascript
复制
<Configuration status="WARN">
    <Appenders>
        <!-- 不重要 -->
        <RollingFile name="ACCESS_LOG" fileName="logs/access.log"
                     filePattern="logs/$${date:yyyy-MM}/access-%d{yyyy-MM-dd}-%i.log.gz">
            <!-- 不重要 -->
        </RollingFile>
    </Appenders>

    <Loggers>
        <Root level="INFO"/>
        <Logger name="com.myproject.commons.AccessLog" level="trace" additivity="false">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="ACCESS_LOG"/>
        </Logger>
    </Loggers>
</Configuration>

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

image-20221118111846952

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

代码语言:javascript
复制
public class AccessLogConfigExtend extends AbstractLogExtend {

    @Override
    public String logConfig() {
        return "classpath:com/github/open/casslog/accesslog/accesslog-log4j.xml";
    }

}

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

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

代码语言:javascript
复制
@Configuration
@ConditionalOnProperty(value = "casslog.accessLogEnabled")
public class AccessLogAutoConfiguration {

    @Bean
    public AccessLogConfigExtend accessLogConfigExtend() {
        return new AccessLogConfigExtend();
    }

}

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

小结

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-11-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 杨同学technotes 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、扩展配置的需求来源
  • 二、看看开源项目 Nacos 的实现
  • 三、即学即用
  • 小结
相关产品与服务
日志服务
日志服务(Cloud Log Service,CLS)是腾讯云提供的一站式日志服务平台,提供了从日志采集、日志存储到日志检索,图表分析、监控告警、日志投递等多项服务,协助用户通过日志来解决业务运维、服务监控、日志审计等场景问题。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档