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

692 阅读5分钟

前言

在前面的文章中,我们已经实现了一个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

github地址:honey-tracing

正文

一. 链路日志格式回顾

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

{
    "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来作为链路日志对应的实体对象,如下所示。

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字段对应的实体对象,实现如下。

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的实现,如下所示。

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时,才会打印链路日志,这样就能确保在一次链路请求中,一个节点,只会打印一条链路日志。

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

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打印链路日志如下。

{
    "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打印链路日志如下。

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

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

总结

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

Starter包工程结构图