[日志] lo4j2之自定义日志格式变量

发布时间 2023-12-28 14:48:44作者: 千千寰宇

1 PatternLayout / LogEventPatternConverter : 自定义日志格式及格式变量

在 Log4j 或 Logback 等 Java 日志框架中,PatternLayout 类允许你定义日志输出的格式。
PatternLayout 通过一系列的转换器(PatternConverter) 来定义输出的样式。其中,LogEventPatternConverter是一种转换器,用于将LogEvent中的信息转换为文本。

以Log4j2为例,下面是一些常用的 LogEventPatternConverter的格式说明。

  • %m : 输出日志消息
  • %p : 输出日志级别
  • %c : 输出日志记录器名称
  • %d : 输出日志时间。可以通过指定日期格式来自定义输出格式
    • 例如:%d{yyyy-MM-dd HH:mm:ss}
  • %t : 输出线程名称
  • %n : 输出一个平台特定的换行符
  • %C : 输出日志记录器的全类名
  • %F : 输出产生日志记录事件的源文件的文件名

这些格式可以按照需要进行组合。以下是一个简单的例子:

log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] %c - %m %n

#%thread [%traceId] [${application.name}] [system] [%d{yyyy/MM/dd HH:mm:ss.SSS}] [%-5p] [%t] [%C{1}] %M:%L__|%X{traceId}|__%m%n
# 注1: thread 是我自定义的 ThreadLogEventPatternConverter 的日志格式变量
# 注2: X{traceId} 是我基于org.slf4j.MDC.put/remove("traceId", traceId) 自定义的日志格式变量

需注意,具体的格式和转换器可能会因使用的日志框架及版本而有所不同。请查阅日志框架的相应文档,获取更详细的信息。

2 案例实践

2.1 基于 LogEventPatternConverter 自定义日志格式转换器及日志格式变量:thread/Thread

2.1.1 ThreadLogEventPatternConverter extends LogEventPatternConverter

import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.apache.logging.log4j.core.pattern.ConverterKeys;
import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
import org.apache.logging.log4j.message.Message;

/**
 * org.apache.logging.log4j.core.pattern.MethodLocationPatternConverter
 * @reference-doc
 *  [1] log4j2 源代码片段 - 编写Converter类型的插件 - CSDN - https://blog.csdn.net/zhouzhiande/article/details/111238677
 */

@Plugin(name = "ThreadLogEventPatternConverter", category = PatternLayout.KEY, printObject = true)
@ConverterKeys({"thread", "Thread"}) // 对应模板中的 %thread / %Thread 的日志格式变量
public class ThreadLogEventPatternConverter extends LogEventPatternConverter {
    private static final ThreadLogEventPatternConverter INSTANCE = new ThreadLogEventPatternConverter();

    private ThreadLogEventPatternConverter() {
        super("threadx", "threadxxxx");
    }

    @Override
    public void format(LogEvent event, StringBuilder toAppendTo) {
        final StackTraceElement sourceStackTraceElement = event.getSource();

        Message message = event.getMessage();
        //String resultMessage = message.getFormattedMessage();//%m

        if (sourceStackTraceElement != null) {
            toAppendTo.append("[" + event.getThreadId() + " | " + event.getThreadName() +  " | " + Thread.currentThread().getId() + "]");
        }
    }

    public static ThreadLogEventPatternConverter newInstance(final String[] options) {
        return INSTANCE;
    }
}

2.1.2 日志框架版本及日志输出策略

  • 日志框架版本
  • slf4j : 1.7.25
  • log4j2 : 2.20.0
  • 日志输出策略(log4j2.properties)
##################### [0] 自定义配置(可灵活修改) #####################
property.instance.name=${env:INSTANCE_NAME:-localInstance}

property.log.level=${env:LOG_ACCESS:-INFO}
property.log.access.level=${env:LOG_ACCESS:-INFO}
property.log.operation.level=${env:LOG_OPERATE:-INFO}
property.log.threshold=${log.level}

property.log.consoleAppender=CONSOLE_APPENDER
#property.log.layout=org.apache.log4j.PatternLayout
property.log.layout=PatternLayout
property.log.layout.consolePattern=%thread [%traceId] [${application.name}] [system] [%d{yyyy/MM/dd HH:mm:ss.SSS}] [%-5p] [%t] [%C{1}] %M:%L__|%X{traceId}|__%m%n
property.log.layout.mainPattern=${log.layout.consolePattern}
property.log.dir=/logs/${instance.name}

##################### [1] 定义 Logger #####################
# ------------------- [1.1] 定义 RootLogger 等 全局性配置(不可随意修改) ------------------- #
## rootLogger, 根记录器,所有记录器的父辈
## 指定根日志的级别 | All < Trace < Debug < Info < Warn < Error < Fatal < OFF
rootLogger.level=${log.level}
## 指定输出的appender引用
## 2.17.2 版本以下通过这种方式将 root 和 Appender关联起来
## 2.17.2 版本以上有更简便的写法
rootLogger.appenderRef.stdout.ref=${log.consoleAppender}
rootLogger.appenderRef.rolling.ref=${log.systemFileAppender}

# ------------------- [1.2] 指定个别 Class 的 Logger (可随意修改,建议在 nacos 上修改) ------------------- #
## [format] step1 若想对不同的类输出不同的文件(以cn.com.Test为例),先要在Test.java中定义: private static Log logger = LogFactory.getLog(Test.class);
## [format] step2 然后在log4j.properties中加入: (即 让 cn.com.Test 中的 logger 使用 log4j.appender.test所做的配置 )
## [format] step3 property.logger.cn.com.Test= DEBUG, test
## [format]       property.appender.test=org.apache.log4j.FileAppender
## [format]       property.appender.test.File=${myweb.root}/WEB-INF/log/test.log
## [format]       property.appender.test.layout=org.apache.log4j.PatternLayout
## [format]       property.appender.test.layout.ConversionPattern=%d %p [%c] - %m%n

# org.springframework.context
# org.springframework.core
# org.springframework.beans.factory
logger.spring.name=org.springframework
logger.spring.level=WARN
logger.spring.additivity=false
logger.spring.appenderRef.systemRollingFile.ref=MySystemFileAppender
logger.spring.appenderRef.console.ref=CONSOLE_APPENDER

logger.mybatisplus.name=com.baomidou.mybatisplus
logger.mybatisplus.level=WARN
logger.mybatisplus.additivity=false
logger.mybatisplus.appenderRef.systemRollingFile.ref=MySystemFileAppender
logger.mybatisplus.appenderRef.console.ref=CONSOLE_APPENDER

logger.mybatisplus.name=com.baomidou.mybatisplus
logger.mybatisplus.level=WARN
logger.mybatisplus.additivity=false
logger.mybatisplus.appenderRef.systemRollingFile.ref=MySystemFileAppender
logger.mybatisplus.appenderRef.console.ref=CONSOLE_APPENDER

##################### [2] 定义 Appender #####################

# ------------------- [2.1] CONSOLE Appender ------------------- #
# console
# 指定输出源的类型与名称
appender.console.type=Console
appender.console.name=CONSOLE_APPENDER
appender.console.layout.type=PatternLayout
appender.console.layout.pattern=${log.layout.consolePattern}

# ------------------- [2.2] SYSTEM File Appender ------------------- #

appender.systemRollingFile.type=RollingFile
appender.systemRollingFile.name=MySystemFileAppender
appender.systemRollingFile.fileName=${log.dir}/system.log
appender.systemRollingFile.filePattern=${log.dir}/system-%d{MM-dd-yyyy}-%i.log.gz
appender.systemRollingFile.policies.type=Policies
appender.systemRollingFile.policies.time.type=TimeBasedTriggeringPolicy
appender.systemRollingFile.policies.size.type=SizeBasedTriggeringPolicy
appender.systemRollingFile.policies.size.size=128MB
appender.systemRollingFile.strategy.type=DefaultRolloverStrategy
appender.systemRollingFile.layout.type=PatternLayout
appender.systemRollingFile.layout.pattern=${log.layout.mainPattern}

# ------------------- [2.3] ACCESS File Appender ------------------- #

appender.accessRollingFile.type=RollingFile
appender.accessRollingFile.name=MyAccessFileAppender
appender.accessRollingFile.fileName=${log.dir}/access.log
appender.accessRollingFile.filePattern=${log.dir}/access-%d{MM-dd-yyyy}-%i.log.gz
appender.accessRollingFile.policies.type=Policies
appender.accessRollingFile.policies.time.type=TimeBasedTriggeringPolicy
appender.accessRollingFile.policies.size.type=SizeBasedTriggeringPolicy
appender.accessRollingFile.policies.size.size=128MB
appender.accessRollingFile.strategy.type=DefaultRolloverStrategy
appender.accessRollingFile.layout.type=PatternLayout
appender.accessRollingFile.layout.pattern=${log.layout.mainPattern}

# ------------------- [2.5] SkyWalkingClient Appender ------------------- #
## @reference-doc :
##  [1] https://skywalking.apache.org/docs/skywalking-java/next/en/setup/service-agent/java-agent/application-toolkit-log4j-2.x/#print-trace-id-in-your-logs
##  [2] https://blog.csdn.net/qq_56042039/article/details/125930502
## 依赖 JAR 包 : org.apache.skywalking:apm-toolkit-log4j-2.x:8.7.0 , org.apache.skywalking:apm-toolkit-trace:8.7.0
## org.apache.skywalking.apm.toolkit.log.log4j.v2.x.log.GRPCLogClientAppender
appender.linkTrace.type=GRPCLogClientAppender
appender.linkTrace.name=MySkyWalkingClientAppender
appender.linkTrace.layout.type=PatternLayout
#appender.linkTrace.layout.pattern=[${application.name}] [${instance.name}] [${env:HOST_IP}] [${env:CONTAINER_IP}] [%d{yyyy/MM/dd HH:mm:ss.SSS}] [%traceId] [%-5p] [%t] [%C{1}.java:%L %M] %m%n
appender.linkTrace.layout.pattern=${log.layout.mainPattern}

2.1.3 日志使用与输出 : Test

public class Test {
    private final static Logger logger = LoggerFactory.getLogger(Test.class);

    public static void main(String[] args) {
        logger.info("hello");
    }
}
  • 输出效果如下:
[1 | main | 1] [TID: N/A] [bdp-xxxx-service] [system] [2023/12/28 14:31:51.633] [INFO ] [main] [Test] main:19__||__hello

2.2 Apache Skywalking 的日志插件(apm-toolkit-log4j-2.x)的自定义日志格式变量traceId

  • 相关依赖
  • skywalking.apm-application-toolkit.version : 8.15.0
<!-- apache skywalking | start -->
<!-- Skywalking 的 Layout 所支持的`ConversionPattern`核心参数在`apm-toolkit-log4j-1.x`插件中是`T`(即 `traceId`, 区分大小写[T≠t]),在`apm-toolkit-logback-1.x`插件中是`%X{tid}`,需打印到日志中,才能被 Skywalking OAP 服务的`Collertor`收集并识别请求链路中的日志信息,否则日志收集失败。 -->
<!--        <dependency>
            <groupId>org.apache.skywalking</groupId>
            <artifactId>apm-toolkit-log4j-1.x</artifactId>
            <version>${skywalking.apm-application-toolkit.version}</version>
        </dependency>-->
<dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-log4j-2.x</artifactId>
    <version>${skywalking.apm-application-toolkit.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-trace</artifactId>
    <version>${skywalking.apm-application-toolkit.version}</version>
</dependency>

<!--
    WebFluxSkyWalkingOperators : apm-toolkit-webflux : start version 8.15.0
    https://github.com/apache/skywalking/discussions/10686 -->
<dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-webflux</artifactId>
    <version>${skywalking.apm-application-toolkit.version}</version>
</dependency>

<!-- apache skywalking [start] -->
<!-- Skywalking 的 Layout 所支持的`ConversionPattern`核心参数在`apm-toolkit-log4j-1.x`插件中是`T`(即 `traceId`, 区分大小写[T≠t]),在`apm-toolkit-logback-1.x`插件中是`%X{tid}`,需打印到日志中,才能被 Skywalking OAP 服务的`Collertor`收集并识别请求链路中的日志信息,否则日志收集失败。 -->
<dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-agent-core</artifactId>
    <version>${skywalking.apm.version}</version>
</dependency>
<!-- apache skywalking | end -->
  • org.apache.skywalking.apm.toolkit.log.log4j.v2.x.TraceIdConverter

apm-toolkit-trace-8.15.0.jar

  • log4j2.properties :日志格式变量traceId的使用

  • 日志输出效果

[TID: 15211e39736b44a9a42f13927fae2f2e.1.17037328207610001] [bdp-gateway-service] [system] [2023/12/28 11:07:03.834] [INFO ] [main] [HikariDataSource] $sw$original$getConnection$08spks0:123__||__HikariPool-1 - Start completed.

2.3 基于 MDC与Spring Cloud Gateway#GlobalFilter的TraceGlobalFilter实现自定义日志格式变量X{traceId}

import org.apache.skywalking.apm.toolkit.trace.TraceContext;
import org.apache.skywalking.apm.toolkit.webflux.WebFluxSkyWalkingOperators;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.util.ObjectUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

public class TraceGlobalFilter implements GlobalFilter /**, Ordered **/{
    private final static Logger logger = LoggerFactory.getLogger(TraceGlobalFilter.class);

    private final static String TRACE_ID_HTTP_HEADER_PARAM = "trace-id";

    public final static String TRACE_ID_MDC_PARAM = "traceId";

    private Integer order;

    public TraceGlobalFilter(Integer order){
        this.order = order;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String traceId = WebFluxSkyWalkingOperators.continueTracing(exchange, TraceContext::traceId);
        if(ObjectUtils.isEmpty(traceId)){//get trace id from skywalking framework is empty
            traceId = SkywalkingUtil.getTraceId(exchange.getRequest().getHeaders());
            logger.info("TraceId from skywalking framework is empty! But traceId from request.headers now is : {}", traceId);
        }
        MDC.put(TRACE_ID_MDC_PARAM, traceId);
        exchange.getResponse().getHeaders().set(TRACE_ID_HTTP_HEADER_PARAM, traceId);
        return chain.filter(exchange).doAfterTerminate( () -> {
            MDC.remove(TRACE_ID_MDC_PARAM);
        });
    }
}
  • log4j2.properties
property.log.layout.consolePattern=[${application.name}] [system] [%d{yyyy/MM/dd HH:mm:ss.SSS}] [%-5p] [%t] [%C{1}] %M:%L__|%X{traceId}|__%m%n

X 参考文献