Skywalking 原生日志集成
<configuration>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%tid] [%thread] %-5level %logger{36} -%msg%n</Pattern>
</layout>
</encoder>
</appender>
<!-- 配置异步记录 AsyncAppender -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>1024</queueSize>
<neverBlock>true</neverBlock>
<appender-ref ref="STDOUT"/>
</appender>
<!-- skywalking grpc 日志收集 8.4.0版本开始支持 -->
<appender name="grpc-log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCPatternLogbackLayout">
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%tid] [%thread] %-5level %logger{36} -%msg%n</Pattern>
</layout>
</encoder>
</appender>
<!--系统操作日志-->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ASYNC"/>
<appender-ref ref="grpc-log"/>
</root>
</configuration>以控制台输出为例:
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%tid] [%thread] %-5level %logger{36} -%msg%n</Pattern>
</layout>
</encoder>
</appender>可以看到此处自定义的内容是定义了一套自己的TraceIdPatternLogbackLayout,然后这个 layout 内部进行 Pattern 的转换,将我们自定义的 tid 转换为 Skywalking 的真实 tid(traceId)
那么我们继续看下TraceIdPatternLogbackLayout的代码实现
apm-toolkit-logback-1.x 项目
TraceIdPatternLogbackLayout

此处定义了针对不同的 key,对应的转换类信息,以上可以看到,针对 tid 则对应的转换类是:LogbackPatternConverter.class.getName()

查看我们自定义类LogbackPatternConverter所对应的父类:logback 的 PatternLayout
可以看到 logback 中针对不同的定义字段,都是设置了对应的 Converter 的解析类。所以上述我们的 Layout 其实也是基于这种模式去执行的。
继续看我们 tid 对应的转化类:LogbackPatternConverter 的实现
LogbackPatternConverter

可以看到该类主要是 继承 ClassicConverter 并重写 convert 方法;
此处 convert 方法默认是直接返回 "TID: N/A"。
为什么不直接调用 Skywalking 的getTraceId() 方法?
这里的主要考量点是,当前该服务模块 apm-toolkit-logback-1.x 本身就是给业务接入的logback 的依赖jar 包;
如果该模块需要获取 TraceId,那么就默认需要再依赖 apm-toolkit-trace 这个 jar 包模块。
这就非常没必要了,这样就变成了,引入 logback 的日志插件包,业务项目中就默认也依赖了 trace 模块;然而业务方可能就没有实际需求引入该 trace 模块。。
除了依赖上述 trace 模块获取 tid 外,另外一种获取 trace 的方式是什么?那就是依赖 skywalking 的 agent-core 模块,这个模块中具备Skywalking 的所有核心 API。
但是我一个对外 SDK 的 JAR 包肯定是没必要依赖 agent-core 这样一个模块的;
所以 Skwalking 的最终做法则是一个比较优雅且合理的方式:
在 Agent 中定义一个 logback 的插件,直接切当前 LogbackPatternConverter 类的 convert()方法,进行字节码增强。
在 Agent 中获取当前上下文的 Traceid,直接注入给当前的 convert()方法返回结果就行。
这样就优雅很多了。
在业务项目不加载 Agent 的时候,尽管依赖了 apm-toolkit-logback-1.x 的 jar 包,在默认输出的时候也只会输出空的 TID;只有在 Agent 生效的时候才会输出真正的 TID;
这样做的一个非常一个大的好处是,所有对外提供的 API SDK,都不需要依赖 Skywalking 的核心 core 包。
所有和 Skywalking 核心 Core 包有交互的,比如获取 traceId,spanId,这些能力只需要在 API 中提供一个空方法即可。业务上使用该 API 的时候就直接使用就行。
当Sky 的 Agent 被真正加载的时候,Agent 会去切这些空的 API 方法,给这个方法提供真实的业务能力。
所以我们此时就需要看一下,LogbackPatternConverter 这个类的 convert 方法是在哪里定义被 Agent 切入的
apm-toolkit-logback-1.x-activation 项目
LogbackPatternConverterActivation

PrintTraceIdInterceptor

此处具体的拦截类做的事情,则是在 afterMethod()方法;
这里的含义是在拦截 convert()方法内容执行后(方法代码执行完,但是 return 前),执行此处拦截器的 afterMethod()方法;
该方法做的主要事情则是直接return TID;只是此处的 TID 是从skyWalkingContext.getTraceId() 获取的;
即从当前的链路上下文中获取当前的链路TraceId,然后返回回去即可。
这样 logback 在具体执行 convert 方法进行 tid 的转换时,由于 Agent 已经动态创建了切面插桩,此处就能获取到真实的上下文链路的 TraceId 了。

我们在对应的 apm-toolkit-logback-1.x 的项目中,除了定义了tid 的 convert 转换外,还有对应的 sw_ctx 这个字段。所以如果需要获取对应的 sw 上下文(spanid,serviceName,instanceName)等信息。
则直接在对应 logback 对应的 layout 中配置增加对应的:sw_ctx 的字段渲染即可,这样就可以将对应的sw_ctx 信息也输出到对应的日志当中。
跟 GrayLog 的集成
<appender name="GELF" class="de.siegmar.logbackgelf.GelfUdpAppender">
<graylogHost>IP</graylogHost>
<graylogPort>30177</graylogPort>
<maxChunkSize>508</maxChunkSize>
<useCompression>true</useCompression>
<encoder class="de.siegmar.logbackgelf.GelfEncoder">
<includeRawMessage>true</includeRawMessage>
<includeMarker>true</includeMarker>
<includeMdcData>true</includeMdcData>
<includeCallerData>true</includeCallerData>
<includeRootCauseData>true</includeRootCauseData>
<includeLevelName>true</includeLevelName>
<shortPatternLayout class="ch.qos.logback.classic.PatternLayout">
<pattern>[%X{X-Request-ID}] %m%nopex</pattern>
</shortPatternLayout>
<fullPatternLayout class="ch.qos.logback.classic.PatternLayout">
<!-- <pattern>%m%n</pattern> -->
<!-- <pattern>[%X{X-Request-ID}] %d - [%thread] %-5level %logger{35} - [%line] - %msg%n</pattern> -->
<pattern>${FILE_LOG_PATTERN}</pattern>
</fullPatternLayout>
<numbersAsString>false</numbersAsString>
<staticField>app_name:$graylog.app_name}</staticField>
<staticField>os_arch:$os.arch}</staticField>
<staticField>os_name:$os.name}</staticField>
<staticField>os_version:os.version}</staticField>
<staticField>java_version:java.version}</staticField>
<staticField>address_ip:address.ip}</staticField>
<staticField>env:graylog.env}</staticField>
</encoder>
</appender>GrayLog 中有自己的GelfUdpAppender和GelfEncoder 我们此处需要变更的是将对应的fullPatternLayout属性变更为 Skywalking 的PatternLayout,即上述的TraceIdPatternLogbackLayout 即可
整合后的结果如下:
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="de.siegmar.logbackgelf.GelfEncoder">
<includeRawMessage>true</includeRawMessage>
<includeMarker>true</includeMarker>
<includeMdcData>true</includeMdcData>
<includeCallerData>true</includeCallerData>
<includeRootCauseData>true</includeRootCauseData>
<includeLevelName>true</includeLevelName>
<shortPatternLayout class="ch.qos.logback.classic.PatternLayout">
<pattern>[%X{X-Request-ID}] %m%nopex</pattern>
</shortPatternLayout>
<fullPatternLayout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%tid] [%thread] %-5level %logger{36} -%msg%n</Pattern>
</fullPatternLayout>
<numbersAsString>false</numbersAsString>
<staticField>app_name:graylog.app_name}</staticField>
<staticField>os_arch:os.arch}</staticField>
<staticField>os_name:os.name}</staticField>
<staticField>os_version:os.version}</staticField>
<staticField>java_version:java.version}</staticField>
<staticField>address_ip:address.ip}</staticField>
<staticField>env:graylog.env}</staticField>
</encoder>
</appender>
此处直接使用GelfEncoder,将对应的fullPatternLayout 属性配置为:TraceIdPatternLogbackLayout 即可。
上述之所以使用 ConsoleAppender 目的是为了看下最终GelfEncoder 输出后的内容直接输出到控制台看下具体的内容是什么。
直接将该 layout 整合到GelfUdpAppender中也是可以的。
我们看下最终控制台输出的内容如下:
{
"version": "1.1",
"host": "localhost",
"short_message": "[] invoke PrintFilter start ",
"full_message": "2024-03-19 16:03:55.204 [TID:51cdae345d414b649458fc2bce32d6a7.139.17108354157420001] [XNIO-1 task-1] DEBUG c.i.xplan.webmvc.filter.PrintFilter -invoke PrintFilter start \n",
"timestamp": 1710835435.204,
"level": 7,
"_req.requestURI": "\/key\/test",
"_source_line_number": 42,
"_raw_message": "invoke PrintFilter start ",
"_os_version": "13.4.1",
"_os_arch": "aarch64",
"_source_method_name": "doFilter",
"_env": "sit",
"_req.method": "GET",
"_req.userAgent": "Apifox\/1.0.0 (https:\/\/apifox.com)",
"_app_name": "graylog.app_name_IS_UNDEFINED",
"_level_name": "DEBUG",
"_thread_name": "XNIO-1 task-1",
"_java_version": "1.8.0_372",
"_os_name": "Mac OS X",
"_logger_name": "com.ingeek.xplan.webmvc.filter.PrintFilter",
"_address_ip": "address.ip_IS_UNDEFINED",
"_source_file_name": "PrintFilter.java",
"_req.remoteHost": "0:0:0:0:0:0:0:1",
"_req.requestURL": "http:\/\/localhost:5004\/key\/test",
"_source_class_name": "com.ingeek.xplan.webmvc.filter.PrintFilter"
}其中full_message字段的 TID 就是 Skywalking 的TraceID。
上述 JSON 中的 “_env” & “_app_name” 这些下划线字段,我们直接定义 MDC.put("",""),GrayLog 会自动获取 MDC 的数据填充到对应的JSON KEY 中。
跨线程传递
public String end(HttpServletRequest request) {
logger.error(">>>>>>>>>>>>>>>>>>>>>>>>start error>>>>>>>>>>>>>>>>>>>>>>>>>");
new Thread(RunnableWrapper.of(new Runnable() {
@Override
public void run() {
SpanRef spanRef = Tracer.createLocalSpan("ThreadCreateSpan");
logger.error(">>>>>>>>>>>>>>>>>>>>>>>>end runable >>>>>>>>>>>>>>>>>>>>>>>>>");
spanRef.log(new NullPointerException("Thread NNN end 空指针"));
spanRef.asyncFinish();
Tracer.stopSpan();
}
})).start();
SpanRef spanRef = Tracer.createLocalSpan("CreateSpan");
spanRef.log(new NullPointerException("NNN end 空指针"));
logger.error(">>>>>>>>>>>>>>>>>>>>>>>>end Span>>>>>>>>>>>>>>>>>>>>>>>>>");
spanRef.asyncFinish();
Tracer.stopSpan();
return "OK";
}
跨线程传递只需要使用 Skywalking 提供的跨线程传递类即可;RunnableWrapper
日志层面因为是直接在 Agent 层面获取的 Context 上下文,所以也可以直接获取到对应的 TraceId 直接输出到对应的日志中。