前言

在前面两篇实战文章中:

覆盖了可观测中的指标追踪和 metrics 监控,下面理应开始第三部分:日志

但在开始日志之前还是要先将链路追踪和日志结合起来看看应用实际使用的实践。

通常我们排查问题的方式是先查询异常日志,判断是否是当前系统的问题。

如果不是,则在日志中捞出 trace_id 再到链路查询系统中查询链路,看看具体是哪个系统的问题,然后再做具体的排查。

类似于这样:



日志中会打印 trace_idspan_id

如果日志系统做的比较完善的话,还可以直接点击 trace_id 跳转到链路系统里直接查询链路信息。

MDC

这里的日志里关联 trace 信息的做法有个专有名词:MDC:(Mapped Diagnostic Context)。

简单来说就是用于排查问题的上下文信息,通常是由键值对组成,类似于这样的数据:

{
"timestamp" : "2024-08-05 17:27:31.097",
"level" : "INFO",
"thread" : "http-nio-9191-exec-1",
"mdc" : {
"trace_id" : "26242f945af80b044a60226af00211fb",
"trace_flags" : "01",
"span_id" : "3a7842b3e28ed5c8"
},
"logger" : "com.example.demo.DemoApplication",
"message" : "request: name: \"1232\"\n",
"context" : "default"
}

在 Java 中的 Log4j 和 Logback 都有提供对应的实现。

如果我们使用了 OpenTelemetry 提供的 javaagent 再配合 logback 或者 Log4j 时就会自动具备打印 MDC 的能力:

java -javaagent:/Users/chenjie/Downloads/blog-img/demo/opentelemetry-javaagent-2.4.0-SNAPSHOT.jar xx.jar

比如我们只需要这样配置这样一个JSON 输出的 logback 即可:

<appender name="PROJECT_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${PATH}/demo.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>${PATH}/demo_%i.log</fileNamePattern>
<maxIndex>1</maxIndex>
</rollingPolicy> <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>100MB</maxFileSize>
</triggeringPolicy> <layout class="ch.qos.logback.contrib.json.classic.JsonLayout">
<jsonFormatter
class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">
<prettyPrint>true</prettyPrint>
</jsonFormatter>
<timestampFormat>yyyy-MM-dd' 'HH:mm:ss.SSS</timestampFormat>
</layout> </appender> <root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="PROJECT_LOG"/>
</root>

就会在日志文件中输出 JSON 格式的日志,并且带上 MDC 的信息。

自动 MDC 的原理

我也比较好奇 OpenTelemetry 是如何自动写入 MDC 信息的,这里以 logback 为例。

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return implementsInterface(named("ch.qos.logback.classic.spi.ILoggingEvent"));
} @Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
isMethod()
.and(isPublic())
.and(namedOneOf("getMDCPropertyMap", "getMdc"))
.and(takesArguments(0)),
LoggingEventInstrumentation.class.getName() + "$GetMdcAdvice");
}

会在调用 ch.qos.logback.classic.spi.ILoggingEvent.getMDCPropertyMap()/getMdc() 这两个函数中进行埋点。

这些逻辑都是写在 javaagent 中的。

public Map<String, String> getMDCPropertyMap() {
// populate mdcPropertyMap if null
if (mdcPropertyMap == null) {
MDCAdapter mdc = MDC.getMDCAdapter();
if (mdc instanceof LogbackMDCAdapter)
mdcPropertyMap = ((LogbackMDCAdapter) mdc).getPropertyMap();
else
mdcPropertyMap = mdc.getCopyOfContextMap();
}
// mdcPropertyMap still null, use emptyMap()
if (mdcPropertyMap == null)
mdcPropertyMap = Collections.emptyMap(); return mdcPropertyMap;
}

这个函数其实默认情况下会返回一个 logback 内置 MDC 的 map 数据(这里的数据我们可以自定义配置)。

而这里要做的就是将 trace 的上下文信息写入这个 mdcPropertyMap 中。

以下是 OpenTelemetry agent 中的源码:

Map<String, String> spanContextData = new HashMap<>();  

SpanContext spanContext = Java8BytecodeBridge.spanFromContext(context).getSpanContext();  

if (spanContext.isValid()) {
spanContextData.put(traceIdKey(), spanContext.getTraceId());
spanContextData.put(spanIdKey(), spanContext.getSpanId());
spanContextData.put(traceFlagsKey(), spanContext.getTraceFlags().asHex());
}
spanContextData.putAll(ConfiguredResourceAttributesHolder.getResourceAttributes()); if (LogbackSingletons.addBaggage()) {
Baggage baggage = Java8BytecodeBridge.baggageFromContext(context); // using a lambda here does not play nicely with instrumentation bytecode process
// (Java 6 related errors are observed) so relying on for loop instead for (Map.Entry<String, BaggageEntry> entry : baggage.asMap().entrySet()) {
spanContextData.put(
// prefix all baggage values to avoid clashes with existing context
"baggage." + entry.getKey(), entry.getValue().getValue());
}} if (contextData == null) {
contextData = spanContextData;
} else {
contextData = new UnionMap<>(contextData, spanContextData);
}

这就是核心的写入逻辑,从这个代码中也可以看出直接从上线文中获取的 span 的 context,而我们所需要的 trace_id/span_id 都是存放在 context 中的,只需要 get 出来然后写入进 map 中即可。

从源码里还得知,只要我们开启 -Dotel.instrumentation.logback-mdc.add-baggage=true 配置还可以将 baggage 中的数据也写入到 MDC 中。

而得易于 OpenTelemetry 中的 trace 是可以跨线程传输的,所以即便是我们在多线程里打印日志时 MDC 数据依然可以准确无误的传递。

MDC 的原理

public static final String MDC_ATTR_NAME = "mdc";

logback 的实现中是会调用刚才的 getMDCPropertyMap() 然后写入到一个 key 为 mdcmap 里,最终可以写入到文件或者控制台。

这样整个原理就可以串起来了。

自定义日志 数据

提到可以自定义 MDC 数据其实也是有使用场景的,比如我们的业务系统经常有类似的需求,需要在日志中打印一些常用业务数据:

  • userId、userName
  • 客户端 IP等信息时

此时我们就可以创建一个 Layout 类来继承 ch.qos.logback.contrib.json.classic.JsonLayout:

public class CustomJsonLayout extends JsonLayout {
public CustomJsonLayout() {
} protected void addCustomDataToJsonMap(Map<String, Object> map, ILoggingEvent event) {
map.put("user_name", context.getProperty("userName"));
map.put("user_id", context.getProperty("userId"));
map.put("trace_id", TraceContext.traceId());
}
} public class CustomJsonLayoutEncoder extends LayoutWrappingEncoder<ILoggingEvent> {
public CustomJsonLayoutEncoder() {
}
public void start() {
CustomJsonLayout jsonLayout = new CustomJsonLayout();
jsonLayout.setContext(this.context);
jsonLayout.setIncludeContextName(false);
jsonLayout.setAppendLineSeparator(true);
jsonLayout.setJsonFormatter(new JacksonJsonFormatter());
jsonLayout.start();
super.setCharset(StandardCharsets.UTF_8);
super.setLayout(jsonLayout);
super.start();
}}

这里的 trace_id 是之前使用 skywalking 的时候由 skywalking 提供的函数:org.apache.skywalking.apm.toolkit.trace.TraceContext#traceId

接着只需要在 logback.xml 中配置这个 CustomJsonLayoutEncoder 就可以按照我们自定义的数据输出日志了:

<appender name="PROJECT_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${PATH}/app.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>${PATH}/app_%i.log</fileNamePattern>
<maxIndex>1</maxIndex>
</rollingPolicy> <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>100MB</maxFileSize>
</triggeringPolicy> <encoder class="xx.CustomJsonLayoutEncoder"/>
</appender> <root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="PROJECT_LOG"/>
</root>

虽然这个功能也可以使用日志切面来打印,但还是没有直接在日志中输出更加方便,它可以直接和我们的日志关联在一起,只是多加了这几个字段而已。

Spring Boot 使用

OpenTelemetry 有给 springboot 应用提供一个 spring-boot-starter 包,用于在不使用 javaagent 的情况下也可以自动埋点。

<dependencies>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
<version>OPENTELEMETRY_VERSION</version>
</dependency>
</dependencies>

但在早期的版本中还不支持直接打印 MDC 日志:

最新的版本已经支持

即便已经支持默认输出 MDC 后,我们依然可以自定义的内容,比如我们想修改一下 key 的名称,由 trace_id 修改为 otel_trace_id 等。

<appender name="OTEL" class="io.opentelemetry.instrumentation.logback.mdc.v1_0.OpenTelemetryAppender">
<traceIdKey>otel_trace_id</traceIdKey>
<spanIdKey>otel_span_id</spanIdKey>
<traceFlagsKey>otel_trace_flags</traceFlagsKey>
</appender>

还是和之前类似,修改下 logback.xml 即可。



他的实现逻辑其实和之前的 auto instrument 中的类似,只不过使用的 API 不同而已。

auto instrument 是直接拦截代码逻辑修改 map 的返回值,而 OpenTelemetryAppender 是继承了 ch.qos.logback.core.UnsynchronizedAppenderBase 接口,从而获得了重写 MDC 的能力,但本质上都是一样的,没有太大区别。

不过使用它的前提是我们需要引入以下一个依赖:

<dependencies>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-logback-mdc-1.0</artifactId>
<version>OPENTELEMETRY_VERSION</version>
</dependency>
</dependencies>

如果不想修改 logback.yaml ,对于 springboot 来说还有更简单的方案,我们只需要使用以下配置即可自定义 MDC 数据:

logging.pattern.level = trace_id=%mdc{trace_id} span_id=%mdc{span_id} trace_flags=%mdc{trace_flags} %5p

这里的 key 也可以自定义,只要占位符没有取错即可。

使用这个的前提是需要加载 javaagent,因为这里的数据是 javaagent 里写进去的。

总结

以上就是关于 MDCOpenTelemetry 中的使用,从使用和源码逻辑上都分析了一遍,希望对 MDCOpenTelemetry 的理解更加深刻一些。

关于 MDC 相关的概念与使用还是很有用的,是日常排查问题必不可少的一个工具。

日志与追踪的完美融合:OpenTelemetry MDC 实践指南的更多相关文章

  1. 再也不担心写出臃肿的Flink流处理程序啦,发现一款将Flink与Spring生态完美融合的脚手架工程-懒松鼠Flink-Boot

    目录 你可能面临如下苦恼: 接口缓存 重试机制 Bean校验 等等...... 它为流计算开发工程师解决了 有了它你的代码就像这样子: 仓库地址:懒松鼠Flink-Boot 1. 组织结构 2. 技术 ...

  2. 懒松鼠Flink-Boot(Flink+Spring):一款将Flink与Spring生态完美融合的脚手架工程

    目录 你可能面临如下苦恼: 接口缓存 重试机制 Bean校验 等等...... 它为流计算开发工程师解决了 有了它你的代码就像这样子: 仓库地址:懒松鼠Flink-Boot 1. 组织结构 2. 技术 ...

  3. MacOSX和Windows 8的完美融合

    MacOSX和Windows8的完美融合 一般情况下我们要在MACOS系统下运行Windows软件怎么办呢?一种方法我们可以装CrossOver这款软件,然后在configuration->in ...

  4. SpringBoot专题1----springboot与mybatis的完美融合

    springboot大家都知道了,搭建一个spring框架只需要秒秒钟.下面给大家介绍一下springboot与mybatis的完美融合: 首先:创建一个名为springboot-mybatis的ma ...

  5. SpringBoot之微服务日志链路追踪

    SpringBoot之微服务日志链路追踪 简介 在微服务里,业务出现问题或者程序出的任何问题,都少不了查看日志,一般我们使用 ELK 相关的日志收集工具,服务多的情况下,业务问题也是有些难以排查,只能 ...

  6. atitit. 日志系统的原则and设计and最佳实践(1)-----原理理论总结.

    atitit. 日志系统的原则and设计and最佳实践总结. 1. 日志系统是一种不可或缺的单元测试,跟踪调试工具 1 2. 日志系统框架通常应当包括如下基本特性 1 1. 所输出的日志拥有自己的分类 ...

  7. 日志收集(ElasticSearch)串联查询 MDC

    之前写过将应用程序或服务程序产生的日志直接写入搜索引擎的博客 其中基本过程就是  app->redis->logstash->elasticsearch 整个链路过程  本来想将re ...

  8. Spring Cloud与Dubbo的完美融合之手「Spring Cloud Alibaba」

    很早以前,在刚开始搞Spring Cloud基础教程的时候,写过这样一篇文章:<微服务架构的基础框架选择:Spring Cloud还是Dubbo?>,可能不少读者也都看过.之后也就一直有关 ...

  9. Java日志Log4j或者Logback的NDC和MDC功能

    NDC和MDC的区别 Java中使用的日志的实现框架有很多种,常用的log4j和logback以及java.util.logging,而log4j是apache实现的一个开源日志组件(Wrapped ...

  10. dubbo traceId透传实现日志链路追踪(基于Filter和RpcContext实现)

    一.要解决什么问题: 使用elk的过程中发现如下问题: 1.无法准确定位一个请求经过了哪些服务 2.多个请求线程的日志交替打印,不利于查看按时间顺序查看一个请求的日志. 二.期望效果 能够查看一个请求 ...

随机推荐

  1. springboot实现登录demo

    实现简单的登录功能 实体类 定义实体类为User3类. 使用@Data:提供类的get,set,equals,hashCode,canEqual,toString方法: 使用@AllArgsConst ...

  2. [oeasy]python0033_任务管理_jobs_切换任务_进程树结构_fg

    ​ 查看进程 回忆上次内容 上次先进程查询 ps -elf 查看所有进程信息 ps -lf 查看本终端相关进程信息 杀死进程 kill -9 PID 给进程发送死亡信号 运行多个 python3 sh ...

  3. AngleScript语法

    Class的使用要继承于Interface或者Mixin class.Mixinclass实际上就是类似于抽象类 ,它已经实现的,在子类里面不能实现,类似如下代码: interface AInterf ...

  4. 关于写作那些事之快速上手Mermaid流程图

    本文主要介绍了如何快速上手 Mermaid 流程图,不用贴图上传也不用拖拉点拽绘制,基于源码实时渲染流程图,操作简单易上手,广泛被集成于主流编辑器,包括 markdown 写作环境. 通过本节内容你将 ...

  5. 代码随想录Day2

    209.长度最小的子数组 给定一个含有 n 个正整数的数组和一个正整数 target . 找出该数组中满足其总和大于等于 target 的长度最小的 子数组 $ [nums_l, nums_{l+1} ...

  6. 4、Git之分支操作

    4.1.分支的概述 在版本控制过程中,当需要同时推进多个任务时,可以为每个任务创建的单独分支. 虽然分支的底层实现是指针的引用,但是初学阶段可以将分支简单理解为副本,一个分支就是一个单独的副本. 使用 ...

  7. 【Vue2】Vue-Cli使用

    1.需要NodeJS环境支持,此处省略NodeJS安装 2.使用NPM命令安装CLI包 vue-cli是npm.上的一个全局包,使用npm install 命令,即可方便的把它安装到自己的电脑上: n ...

  8. 【Layui】16 表单元素 Form

    文档地址: https://www.layui.com/demo/form.html 表单元素: 1.输入框 2.密码框 3.下拉列表 4.单选框 5.复选框 6.文档域 7.富文本 8.开关 单行输 ...

  9. pytorch中神经网络的多线程数设置:torch.set_num_threads(N)

    实验室的同学一直都是在服务器上既用CPU训练神经网络也有使用GPU的,最近才发现原来在pytorch中可以通过设置  torch.set_num_threads(args.thread)  来限制CP ...

  10. Tensorflow1.14中placeholder.shape和tf.shape(placeholder)的区别

    最近在看TensorFlow的代码,还是1.14版本的TensorFlow的,代码难度确实比pytorch的难上不是多少倍,pytorch的代码看一遍基本能看懂个差不多,TensorFlow的代码看一 ...