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 参考文献
- log4j2 源代码片段 - 编写Converter类型的插件 - CSDN 【推荐】
- Lo4j2 重写日志,Lo4j2日志 脱敏思路 - CSDN 【推荐】
- log4j2.xml文件讲解和在日志中加入全局guid - 代码先锋网