首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

日志与追踪的完美融合:OpenTelemetry MDC 实践指南

编程知识
2024年09月11日 09:11

前言

在前面两篇实战文章中:

覆盖了可观测中的指标追踪和 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">  
    <file>${PATH}/demo.log</file>  
  
    <rollingPolicy>  
        <fileNamePattern>${PATH}/demo_%i.log</fileNamePattern>  
        <maxIndex>1</maxIndex>  
    </rollingPolicy>  
  
    <triggeringPolicy>  
        <maxFileSize>100MB</maxFileSize>  
    </triggeringPolicy>  
  
    <layout>  
        <jsonFormatter  
               >  
            <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">  
    <file>${PATH}/app.log</file>  
  
    <rollingPolicy>  
        <fileNamePattern>${PATH}/app_%i.log</fileNamePattern>  
        <maxIndex>1</maxIndex>  
    </rollingPolicy>  
  
    <triggeringPolicy>  
        <maxFileSize>100MB</maxFileSize>  
    </triggeringPolicy>  
  
    <encoder/>  
</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 日志:
image.png

最新的版本已经支持

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

<appender name="OTEL">
  <traceIdKey>otel_trace_id</traceIdKey>
  <spanIdKey>otel_span_id</spanIdKey>
  <traceFlagsKey>otel_trace_flags</traceFlagsKey>
</appender>

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

image.png
他的实现逻辑其实和之前的 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 相关的概念与使用还是很有用的,是日常排查问题必不可少的一个工具。

From:https://www.cnblogs.com/crossoverJie/p/18407757
本文地址: http://shuzixingkong.net/article/1912
0评论
提交 加载更多评论
其他文章 一文看懂什么是架构
对程序员来说,架构是一个常见词汇。如果想成为一名架构师,对架构概念的理解必须清晰。否则,在制定架构方案时,肯定会漏洞百出,问题频发,这将对你的面试、晋升和团队领导产生负面影响。 我们看下维基百科关于架构的定义: 软件架构是抽象描述系统的一组结构,以及构建这些结构的规则。这些结构包括:软件要素、要素之
一文看懂什么是架构 一文看懂什么是架构
Blazor开发框架Known-V2.0.10
Known今天迎来了2.0的第11个版本,同时网站网址和板块也进行了一次升级改造,虽不完美,但一直在努力改变,之前一直在完善框架功能,忽略了文档的重要性,所以这次更新了文档和API。交流互动板块也在进行当中,尽请期待。 官方网站:http://known.org.cn 最新版本:V2.0.10 下面
Blazor开发框架Known-V2.0.10 Blazor开发框架Known-V2.0.10 Blazor开发框架Known-V2.0.10
Logstash 配置Java日志格式的方法
本文简要介绍了Logstash 是用于日志收集的开源工具,通常与 Elasticsearch 和 Kibana 一起使用,形成 ELK Stack(现在称为 Elastic Stack)。Logstash 非常灵活,可以通过配置文件(通常是.conf文件)来定义数据的输入、处理和输出。对于处理 Ja
VS2022 17.12.0 Preview2版本对Copilot的功能增强
前提条件,使用最新版的17.12.0 Preview2,并且有有效的Copilot AI订阅,那么可以体验这些新鲜好用的功能 增强了Copilot AI对IEnumerable Visualizer的可编辑表达式功能 我们可以通过AI实现一些复杂的条件筛查,并且可以即时验证结果是否符合预期,对于开发
VS2022 17.12.0 Preview2版本对Copilot的功能增强 VS2022 17.12.0 Preview2版本对Copilot的功能增强 VS2022 17.12.0 Preview2版本对Copilot的功能增强
Redis集群slot迁移改造实践
Redis集群经常需要进行在线水平扩缩容,实际操作过程中发现迁移期间服务时延剧烈抖动,业务侧感知明显,为了应对以上问题对原生Redis集群slot迁移功能进行优化改造。
Redis集群slot迁移改造实践 Redis集群slot迁移改造实践 Redis集群slot迁移改造实践
Go runtime 调度器精讲(二):调度器初始化
原创文章,欢迎转载,转载请注明出处,谢谢。 0. 前言 上一讲 介绍了 Go 程序初始化的过程,这一讲继续往下看,进入调度器的初始化过程。 接着上一讲的执行过程,省略一些不相关的代码,执行到 runtime/asm_amd64.s:rt0_go:343L: (dlv) si asm_amd64.s:
Go runtime 调度器精讲(二):调度器初始化 Go runtime 调度器精讲(二):调度器初始化 Go runtime 调度器精讲(二):调度器初始化
ToCom:一次训练随意使用,华为提出通用的ViT标记压缩器 | ECCV 2024
标记压缩通过减少冗余标记的数量(例如,修剪不重要的标记或合并相似的标记)来加快视觉变换器(ViTs)的训练和推理。然而,当这些方法应用于下游任务时,如果训练和推理阶段的压缩程度不匹配,会导致显著的性能下降,这限制了标记压缩在现成训练模型上的应用。因此提出了标记补偿器(ToCom),以解耦两个阶段之间
ToCom:一次训练随意使用,华为提出通用的ViT标记压缩器 | ECCV 2024 ToCom:一次训练随意使用,华为提出通用的ViT标记压缩器 | ECCV 2024 ToCom:一次训练随意使用,华为提出通用的ViT标记压缩器 | ECCV 2024
Java实现英语作文单词扫盲程序
来自背英语四级单词的突发奇想: 是否可以通过Java语言实现一个随机抽取作文中单词进行复习的程序。 成果展示: 点击查看代码 package Demo; import java.util.ArrayList; import java.util.Random; import java.util.Sca