前言
在前面的文章中,我们已经实现了一个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 进行构建,当前节点的信息主要从Span 的SpanContext 和Tags 中获取,下游节点的信息主要从Span 的Logs中获取。
然后我们定义了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实现设计
在TracingFilter 的doFilter() 方法的最后,会调用Span 的finish() 方法,该方法最终会调用到注册到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() {
}
}
有一点需要注意,我们只有当Span 的span.kind 是server时,才会打印链路日志,这样就能确保在一次链路请求中,一个节点,只会打印一条链路日志。
最后给出本文中新使用到的异常对象的实现,如下所示。
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": []
}
可见链路日志是成功打印的。
总结
本文回顾了链路日志的打印格式,并定义了其对应的实体对象,最后实现了打印链路日志的Reporter 。Starter包工程目录结构如下所示。