7. 链路日志打印实现设计

前言

在前面的文章中,我们已经实现了一个Starter 包,能够在使用RestTemplate 作为客户端请求工具时,记录调用链路信息。在本文,将实现Jaeger 框架下的链路日志打印,也就是提供一个io.jaegertracing.spi.Reporter 来将Span的信息打印出来。

相关版本依赖如下。

opentracing-api 版本:0.33.0
opentracing-spring-web 版本:4.1.0
jaeger-client 版本:1.8.1
Springboot 版本:2.7.6

正文

一. 链路日志格式回顾

3. 分布式链路追踪的链路日志设计一文中,定义了要打印的链路日志格式,如下所示。

json 复制代码
{
    "traceId": "testTraceId",                // 当前节点所属链路的Id
    "spanId": "testSpanId",                  // 当前节点的SpanId
    "parentSpanId": "testparentSpanId",      // 当前节点的父节点的SpanId
    "timestamp": "1704038400000",            // 接收到请求那一刻的毫秒时间戳
    "duration": "10",                        // 表示接收请求到响应请求的耗时
    "httpCode": "200",                       // 请求的HTTP状态码
    "host": "127.0.0.1",                     // 当前节点的主机地址
    "requestStacks": [                       // 请求堆栈
        {
            "subSpanId": "testSubSpanId",    // 当前节点的子节点的SpanId
            "subHttpCode": "200",            // 请求子节点的HTTP状态码
            "subTimestamp": "1704038401000", // 当前节点请求子节点的毫秒时间戳
            "subDuration": "5",              // 表示发起请求到收到响应的耗时
            "subHost": "192.168.10.5"        // 当前节点的子节点的主机地址
        }
    ]
}

上述信息中,除了requestStacks 字段以外的字段,都是当前节点的Span 中记录的信息,而requestStacks 则是当前节点请求下游节点,代表下游节点的Span中记录的信息。

二. 链路日志实体对象设计

首先,我们定义HoneySpanReportEntity来作为链路日志对应的实体对象,如下所示。

java 复制代码
public class HoneySpanReportEntity {

    public static final Integer SCALE = 0;

    private String traceId;
    private String spanId;
    private String parentSpanId;
    private String timestamp;
    private String duration;
    private String httpCode;
    private String host;
    private List<HoneyRequestStack> requestStacks = new ArrayList<>();

    private HoneySpanReportEntity() {

    }

    public String toPrintString() {
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            return objectMapper.writeValueAsString(this);
        } catch (Exception e) {
            return StringUtils.EMPTY;
        }
    }

    public void addRequestStack(HoneyRequestStack honeyRequestStack) {
        requestStacks.add(honeyRequestStack);
    }

    // 省略getter和setter

    public static class HoneySpanReportEntityBuilder {
        private JaegerSpan span;

        private HoneySpanReportEntityBuilder() {

        }

        public static HoneySpanReportEntityBuilder builder() {
            return new HoneySpanReportEntityBuilder();
        }

        public HoneySpanReportEntityBuilder withSpan(JaegerSpan span) {
            this.span = span;
            return this;
        }

        public HoneySpanReportEntity build() {
            if (span == null) {
                throw new HoneyTracingException();
            }

            HoneySpanReportEntity honeySpanReportEntity = new HoneySpanReportEntity();

            honeySpanReportEntity.traceId = span.context().getTraceId();
            honeySpanReportEntity.spanId = span.context().toSpanId();
            honeySpanReportEntity.parentSpanId = Utils.to16HexString(span.context().getParentId());
            honeySpanReportEntity.timestamp = new BigDecimal(span.getStart())
                    .divide(BigDecimal.valueOf(1000), SCALE , RoundingMode.DOWN).toString();
            honeySpanReportEntity.duration = new BigDecimal(span.getDuration())
                    .divide(BigDecimal.valueOf(1000), SCALE , RoundingMode.DOWN).toString();

            Map<String, Object> spanTags = span.getTags();
            honeySpanReportEntity.httpCode = String.valueOf(spanTags.get(FIELD_HTTP_CODE));
            honeySpanReportEntity.host = (String) spanTags.get(FIELD_HOST);

            List<LogData> spanLogs = span.getLogs();
            if (span.getLogs() != null) {
                spanLogs.forEach(handleLogData(honeySpanReportEntity));
            }

            return honeySpanReportEntity;
        }

        private Consumer<LogData> handleLogData(HoneySpanReportEntity honeySpanReportEntity) {
            return new Consumer<LogData>() {
                @Override
                public void accept(LogData logData) {
                    if (LOG_EVENT_KIND_REQUEST_STACK.equals(logData.getFields().get(LOG_EVENT_KIND))) {
                        HoneyRequestStack honeyRequestStack = HoneyRequestStack.HoneyRequestStackBuilder
                                .builder()
                                .withLogData(logData)
                                .build();
                        honeySpanReportEntity.addRequestStack(honeyRequestStack);
                    }
                }
            };
        }
    }

}

HoneySpanReportEntity 是通过建造者HoneySpanReportEntityBuilder 进行构建,当前节点的信息主要从SpanSpanContextTags 中获取,下游节点的信息主要从SpanLogs中获取。

然后我们定义了HoneyRequestStack 来作为链路日志中的requestStacks字段对应的实体对象,实现如下。

java 复制代码
public class HoneyRequestStack {

    private String subSpanId;
    private String subHttpCode;
    private String subTimestamp;
    private String subDuration;
    private String subHost;

    private HoneyRequestStack() {

    }

    // 省略getter和setter

    public static class HoneyRequestStackBuilder {
        private LogData logData;

        private HoneyRequestStackBuilder() {

        }

        public static HoneyRequestStackBuilder builder() {
            return new HoneyRequestStackBuilder();
        }

        public HoneyRequestStackBuilder withLogData(LogData logData) {
            this.logData = logData;
            return this;
        }

        public HoneyRequestStack build() {
            if (logData == null || logData.getFields() == null) {
                throw new HoneyTracingException();
            }
            Map<String, ?> logDataFields = logData.getFields();
            HoneyRequestStack honeyRequestStack = new HoneyRequestStack();
            honeyRequestStack.subSpanId = (String) logDataFields.get(FIELD_SUB_SPAN_ID);
            honeyRequestStack.subHttpCode = String.valueOf(logDataFields.get(FIELD_SUB_HTTP_CODE));
            honeyRequestStack.subTimestamp = new BigDecimal(String.valueOf(logDataFields.get(FIELD_SUB_TIMESTAMP)))
                    .divide(BigDecimal.valueOf(1000), SCALE , RoundingMode.DOWN).toString();
            honeyRequestStack.subDuration = new BigDecimal(String.valueOf(logDataFields.get(FIELD_SUB_DURATION)))
                    .divide(BigDecimal.valueOf(1000), SCALE , RoundingMode.DOWN).toString();
            honeyRequestStack.subHost = (String) logDataFields.get(FIELD_SUB_HOST);
            return honeyRequestStack;
        }
    }

}

同样也是基于建造者来构建,并且每有一个包含键为logEventKind ,值为requestStack 的键值对,就会创建一个HoneyRequestStack出来。

三. Reporter实现设计

TracingFilterdoFilter() 方法的最后,会调用Spanfinish() 方法,该方法最终会调用到注册到Tracer 中的Reporter 对象的report() 方法中来,所以本节给出Reporter的实现,如下所示。

java 复制代码
public class HoneySpanReporter implements Reporter {

    public void report(JaegerSpan span) {
        if (Tags.SPAN_KIND_CLIENT.equals(span.getTags().get(Tags.SPAN_KIND.getKey()))) {
            return;
        }

        System.out.println(HoneySpanReportEntity.HoneySpanReportEntityBuilder
                .builder()
                .withSpan(span)
                .build()
                .toPrintString());
    }

    public void close() {

    }

}

有一点需要注意,我们只有当Spanspan.kindserver时,才会打印链路日志,这样就能确保在一次链路请求中,一个节点,只会打印一条链路日志。

最后给出本文中新使用到的异常对象的实现,如下所示。

java 复制代码
public class HoneyTracingException extends RuntimeException {

    public HoneyTracingException() {

    }

    public HoneyTracingException(String message) {
        super(message);
    }

    public HoneyTracingException(String message, Throwable cause) {
        super(message, cause);
    }

    public HoneyTracingException(Throwable cause) {
        super(cause);
    }

    public HoneyTracingException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

}

四. 测试链路日志打印

我们使用4. 分布式链路追踪客户端工具包Starter设计中搭建好的测试demo,来验证我们的链路日志打印。

example-service-1打印链路日志如下。

json 复制代码
{
    "traceId": "c3daffb4096da5907efc352889a8d14c",
    "spanId": "7efc352889a8d14c",
    "parentSpanId": "0000000000000000",
    "timestamp": "1707133953950",
    "duration": "292",
    "httpCode": "200",
    "host": "http://localhost:8080",
    "requestStacks": [
        {
            "subSpanId": "3a65454f65988c9a",
            "subHttpCode": "200",
            "subTimestamp": "1707133954015",
            "subDuration": "206",
            "subHost": "localhost:8081"
        }
    ]
}

example-service-2打印链路日志如下。

json 复制代码
{
    "traceId": "c3daffb4096da5907efc352889a8d14c",
    "spanId": "3a65454f65988c9a",
    "parentSpanId": "7efc352889a8d14c",
    "timestamp": "1707133954093",
    "duration": "27",
    "httpCode": "200",
    "host": "http://localhost:8081",
    "requestStacks": []
}

可见链路日志是成功打印的。

总结

本文回顾了链路日志的打印格式,并定义了其对应的实体对象,最后实现了打印链路日志的ReporterStarter包工程目录结构如下所示。

相关推荐
Yan.love21 分钟前
开发场景中Java 集合的最佳选择
java·数据结构·链表
椰椰椰耶24 分钟前
【文档搜索引擎】搜索模块的完整实现
java·搜索引擎
大G哥24 分钟前
java提高正则处理效率
java·开发语言
小_太_阳1 小时前
Scala_【1】概述
开发语言·后端·scala·intellij-idea
智慧老师1 小时前
Spring基础分析13-Spring Security框架
java·后端·spring
lxyzcm1 小时前
C++23新特性解析:[[assume]]属性
java·c++·spring boot·c++23
V+zmm101341 小时前
基于微信小程序的乡村政务服务系统springboot+论文源码调试讲解
java·微信小程序·小程序·毕业设计·ssm
李洋-蛟龙腾飞公司2 小时前
HarmonyOS Next 应用元服务开发-分布式数据对象迁移数据文件资产迁移
分布式·华为·harmonyos
Oneforlove_twoforjob2 小时前
【Java基础面试题025】什么是Java的Integer缓存池?
java·开发语言·缓存
xmh-sxh-13142 小时前
常用的缓存技术都有哪些
java