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

github地址:honey-tracing

正文

一. 链路日志格式回顾

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

{
    "traceId": "testTraceId",                
    "spanId": "testSpanId",                  
    "parentSpanId": "testparentSpanId",      
    "timestamp": "1704038400000",            
    "duration": "10",                        
    "httpCode": "200",                       
    "host": "127.0.0.1",                     
    "requestStacks": [                       
        {
            "subSpanId": "testSubSpanId",    
            "subHttpCode": "200",            
            "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);
    }

    

    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() {

    }

    

    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包工程目录结构如下所示。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值