看完这篇文章我奶奶都懂Opentracing了 (二)

二. 概念分析

1. Span和SpanContext

结合上述示例,我们从Span开始入手来进行概念分析,但是说在最前面,Span在不同的分布式链路实现中,其定义是不全一样的,尽管Opentracing已经进行了概念的统一,但是具体到各家实现,有时候差异还是有的,故此,我们这里解释Span概念时,会更倾向于Jaeger

一次请求,通常会由分布式系统中的多个工作单元组成,这些工作单元按照调用顺序串起来会形成一条链路,那么一条链路中的某个工作单元,就用Span来表示。但工作单元具体指什么呢,可以这么理解,服务B收到请求并开始处理,这就是一个工作单元,我们可以为这件事情创建一个Span来表示,又比如服务B调用了服务C,这又是一个工作单元,我们可以为调用服务C这件事情创建一个Span。所有Span串联起来就组成一条分布式链路,例如下图这样。

Span千万别想太复杂,简单点想Span就是链路中的被调用或调用行为。通常一个Span需要包含如下的主要信息。

  • Operation Name。可以粗略理解为Span的名称,用于描述当前Span是做什么的;
  • StartTimestampFinishTimestamp。分别表示Span的开始处理时间和结束处理时间;
  • Tags。是一个Map,用于以键值对的形式存储Span在处理业务的过程中的一些数据;
  • Logs。最本质还是一个Map,用于以键值对的形式存储一些数据,在Span处理结束并打印链路日志时会用到;
  • SpanContext。表示Span的上下文,这个说法有点抽象,下面会单独把这个SpanContext拎出来讲一下。

一个Span,最主要的就是上述的这些内容,其中TagsLogs有点抽象,SpanContext较为抽象,但是不慌,首先是TagsLogs,这两兄弟本质就是存储Span在处理业务过程中的一些需要记录的信息,所有内容都可以记录,想记啥就记啥,想记在Tags还是记在Logs全凭自己心情,反正最终要用到这些数据的时候,我们都可以通过Span来拿到,但是TagsLogs也不是完全没有区别,先看一下Jaeger中的Span对象的tags字段的签名,如下所示。

private final Map<String, Object> tags;

是一个简单的Map,然后logs字段要复杂一点,字段签名如下所示。

private List<LogData> logs;@Getter
public final class LogData {private final long time;private final String message;private final Map<String, ?> fields;LogData(long time, String message, Map<String, ?> fields) {this.time = time;this.message = message;this.fields = fields;}
}

乍一看是一个LogData的集合,但其实LogData本质就是对一个Map做了包装,所以一个LogData就是一组键值对,这就让logs相较于tags有一个天然的分组的优势,例如我当前Span某次调用下游的操作的相关数据可以存成一个LogData,我当前Span某次SQL执行的相关数据可以存成一个LogData

现在再来揭开SpanContext的面纱。首先Span是一次请求链路中的某个工作单元的表示,那么对于某个Span来说,有两个东西很重要,其一是标识这次请求链路的traceId,毕竟Span得知道自己是在哪条链路上对吧,其二是标识当前工作单元的spanId,相当于Span在这条链路上的身份证。既然SpanContext作为Span上下文,理所应当的SpanContext就应该保存着对应的链路的traceId以及SpanContext所属SpanspanId,但是仅有traceIdspanId还不够,还得保存一个parentSpanId,当前Span可以通过parentSpanId表示对应链路中自己的上一个工作单元是哪个Span,最后,SpanContext还有一个字段叫做baggage,是一个Map,可以将数据从当前Span传递到下一个Span,即跨进程数据传递。

那么总结一下SpanContext里面的主要信息。

  • traceIdSpan所属链路的唯一标识;
  • spanIdSpan在对应链路上的唯一标识;
  • parentSpanIdSpan在对应链路上的父Span的唯一标识;
  • baggage。以键值对的形式存储数据并进行跨进程传输。

其实无论是traceIdspanId还是baggage,都是需要从一个Span传递到下一个Span的,所以SpanContext主要就是负责跨进程数据传递。

那么到这里,Span是什么,想必在纸面上已经是说明白了七七八八了,下面再来看一下我愿称之为Span发射器的Tracer,先看一下io.opentracing包中提供的Tracer接口的注释,如下所示。

Tracer is a simple, thin interface for Span creation and propagation across arbitrary transports.

Tracer主要干两件事情,其一是创建Span,其二是传递Span,创建Span没什么好说的,传递Span又分同一进程和跨进程传递,至于怎么传递,后面可以结合Jaeger提供的相关实现来看一下。

到此为止,我们已经基本具备去阅读JaegerOpentracing相关实现的源码的基础了,所以现在就接合第一节中的客户端和服务端的示例代码,过一下相关源码实现,这样能更加理解Opentracing的相关概念。

首先再重温一下客户端这边的RestTemplate的拦截器的实现,如下所示。

public class RestTemplateTracingInterceptor implements ClientHttpRequestInterceptor {private final Tracer tracer;public RestTemplateTracingInterceptor(Tracer tracer) {this.tracer = tracer;}@NotNullpublic ClientHttpResponse intercept(@NotNull HttpRequest request, @NotNull byte[] body,ClientHttpRequestExecution execution) throws IOException {Span span = tracer.buildSpan(REST_TEMPLATE_SPAN_NAME).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).start();span.setBaggageItem(REST_TEMPLATE_SPAN_TAG_URI, request.getURI().toString());tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new HttpHeadersCarrier(request.getHeaders()));try (Scope scope = tracer.activateSpan(span)) {return execution.execute(request, body);} catch (IOException e) {Tags.ERROR.set(span, Boolean.TRUE);throw e;} finally {span.finish();}}}

首先我们通过Tracer创建了一个Span,因为使用的是Jaeger,这里的Tracer实际是JaegerTracer,创建出来的Span实际是JaegerSpan。在创建Span时,实际调用的是JaegerTracer.SpanBuilder#start方法,这个方法实现如下所示。

@Override
public JaegerSpan start() {JaegerSpanContext context;if (references.isEmpty() && !ignoreActiveSpan && null != scopeManager.activeSpan()) {asChildOf(scopeManager.activeSpan());}if (references.isEmpty() || !references.get(0).getSpanContext().hasTrace()) {context = createNewContext();} else {context = createChildContext();}long startTimeNanoTicks = 0;boolean computeDurationViaNanoTicks = false;if (startTimeMicroseconds == 0) {startTimeMicroseconds = clock.currentTimeMicros();if (!clock.isMicrosAccurate()) {startTimeNanoTicks = clock.currentNanoTicks();computeDurationViaNanoTicks = true;}} else {verifyStartTimeInMicroseconds();}JaegerSpan jaegerSpan = getObjectFactory().createSpan(JaegerTracer.this,operationName,context,startTimeMicroseconds,startTimeNanoTicks,computeDurationViaNanoTicks,tags,references);if (context.isSampled()) {metrics.spansStartedSampled.inc(1);} else {metrics.spansStartedNotSampled.inc(1);}return jaegerSpan;
}

上面的JaegerTracer.SpanBuilder#start方法在创建Span对象时,关键的地方其实是创建SpanContext,创建有两种情况,如下所示。

  1. 创建新SpanContext。在没有显式指定父Span,并且当前线程之前没有已经存在的Span时,就需要创建新SpanContext,也就意味着会生成新的traceIdspanId,对应的Span也会作为整个链路的首个节点,相应的parentSpanId会置为0,表示没有父Span
  2. 创建子SpanContext。如果显式指定了父Span,或者当前线程之前有已经存在的Span且不能忽略这个已经存在的Span,就需要创建子SpanContext

创建新SpanContext的情况没什么好说的,下面重点分析一下创建子SpanContext,对应方法为SpanBuilder#createChildContext,如下所示。

private JaegerSpanContext createChildContext() {JaegerSpanContext preferredReference = preferredReference();if (isRpcServer()) {if (isSampled()) {metrics.tracesJoinedSampled.inc(1);} else {metrics.tracesJoinedNotSampled.inc(1);}if (zipkinSharedRpcSpan) {return preferredReference;}}return getObjectFactory().createSpanContext(preferredReference.getTraceIdHigh(),preferredReference.getTraceIdLow(),Utils.uniqueId(),preferredReference.getSpanId(),preferredReference.getFlags(),getBaggage(),null);
}

在创建子SpanContext时,Jaeger在满足一定条件下会开启Zipkin的兼容模式,这个时候父SpanContext会直接作为子SpanContext返回,至于这么做有什么用,其实质就是改变了Span的理论模型,但我们这里不必深究,知道有这么回事即可。在更多的时候,创建子SpanContext时其实就是父SpanContexttraceId传递给子SpanContext,然后父SpanContextspanId作为子SpanContextparentSpanId,最后子SpanContext会新生成一个自己的spanId

至此,我们已经了解了Tracer如何创建出Span,那么现在再来看一下io.opentracing中定义的Span接口长什么样,这决定了我们可以基于Span做一些什么操作。

public interface Span {SpanContext context();Span setTag(String key, String value);Span setTag(String key, boolean value);Span setTag(String key, Number value);<T> Span setTag(Tag<T> tag, T value);Span log(Map<String, ?> fields);Span log(long timestampMicroseconds, Map<String, ?> fields);Span log(String event);Span log(long timestampMicroseconds, String event);Span setBaggageItem(String key, String value);String getBaggageItem(String key);Span setOperationName(String operationName);void finish();void finish(long finishMicros);
}

基本就是Span持有什么,我们就能操作什么,比如在客户端的RestTemplate拦截器的示例代码中,我们就手动添加了Baggage键值对,而我们知道,Baggage是存在于SpanContext中并用于跨进程传输的,那么这里要如何做到跨进程传输呢,这里就要引出Opentracing中另外一套很重要的概念,即InjectExtract

2. Inject和Extract

要了解InjectExtract,首先需要看一下Tracer接口中的如下两个方法。

<C> void inject(SpanContext spanContext, Format<C> format, C carrier);<C> SpanContext extract(Format<C> format, C carrier);

上述的inject() 方法和extract() 方法乍一看有点抽象,但是如果用第一节示例代码来解释应该就比较好理解,即客户端这边通过inject() 方法可以将SpanContext中的内容,注入到HTTP请求头中,怎么注入,就是formatcarrier配合完成,然后服务端这边通过extract() 方法可以从HTTP请求头中提取出SpanContext,怎么提取,也是formatcarrier配合完成。在大概知道inject() 方法和extract() 方法的作用后,我们再来看一下formatcarrier,通过方法签名,我们可以知道carrier的类型是由format来决定的,所以formatcarrier肯定是要配合使用的,这里以示例代码中的客户端的RestTemplate拦截器的实现举例,先看如下这一段示例代码。

tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new HttpHeadersCarrier(request.getHeaders()));

先看一下Format.Builtin.HTTP_HEADERS是什么,如下所示。

public final static Format<TextMap> HTTP_HEADERS = new Builtin<TextMap>("HTTP_HEADERS");

上述能说明两个事情:第一就是Format.Builtin.HTTP_HEADERS的名称叫做HTTP_HEADERS,表明这个Format和注入信息到HTTP请求头有关,第二就是和Format.Builtin.HTTP_HEADERS配合使用的carrier的类型是TextMap。不过如果仔细看一下Format.Builtin.HTTP_HEADERS,发现其是一个Builtin对象,这个对象是一个很简单的对象,如下所示。

final class Builtin<C> implements Format<C> {private final String name;private Builtin(String name) {this.name = name;}public final static Format<TextMap> TEXT_MAP = new Builtin<TextMap>("TEXT_MAP");public final static Format<TextMapInject> TEXT_MAP_INJECT = new Builtin<TextMapInject>("TEXT_MAP_INJECT");public final static Format<TextMapExtract> TEXT_MAP_EXTRACT = new Builtin<TextMapExtract>("TEXT_MAP_EXTRACT");public final static Format<TextMap> HTTP_HEADERS = new Builtin<TextMap>("HTTP_HEADERS");public final static Format<Binary> BINARY = new Builtin<Binary>("BINARY");public final static Format<BinaryInject> BINARY_INJECT = new Builtin<BinaryInject>("BINARY_INJECT");public final static Format<BinaryExtract> BINARY_EXTRACT = new Builtin<BinaryExtract>("BINARY_EXTRACT");@Overridepublic String toString() {return Builtin.class.getSimpleName() + "." + name;}
}

那反正我是无论如何都无法想明白Builtin对象怎么和TextMap扯上关系,直到我发现其实每一个Format都有一个配套的Injector以及Extractor,例如默认情况下,Format.Builtin.HTTP_HEADERS配套的InjectorExtractor都是TextMapCodec,所以下面就分析一下TextMapCodec,先看一下类图,如下所示。

首先TextMapCodec分别实现了InjectorExtractor接口,对应的实现方法签名如下所示。

public void inject(JaegerSpanContext spanContext, TextMap carrier)public JaegerSpanContext extract(TextMap carrier)

那么现在就逐渐清晰明了了,我们之前困惑format对象怎么和carrier扯上关系,其实就是首先根据format对象拿到与之配对的InjectorExtractor,然后在InjectorExtractor中会调用到carrier对象来完成注入和提取的操作,那么具体到我们的示例里,format对象是Format.Builtin.HTTP_HEADERS,所以对应的carrier类型是TextMap,且其配对的InjectorExtractor均是TextMapCodec,然后TextMapCodec实现的inject()extract() 的入参carrier的类型也均是TextMap,所以Format.Builtin.HTTP_HEADERS就可以处理类型是TextMapcarrier

那么示例代码中,客户端RestTemplate拦截器中,使用的formatFormat.Builtin.HTTP_HEADERScarrierHttpHeadersCarrier,所以如何将SpanContext的内容写到HTTP请求头,需要分析一下TextMapCodec#inject方法,如下所示。

@Override
public void inject(JaegerSpanContext spanContext, TextMap carrier) {carrier.put(contextKey, contextAsString(spanContext));for (Map.Entry<String, String> entry : spanContext.baggageItems()) {carrier.put(keys.prefixedKey(entry.getKey(), baggagePrefix), encodedValue(entry.getValue()));}
}

我们知道上述方法中的carrier实际类型为HttpHeadersCarrier,所以看一下HttpHeadersCarrier#put方法干了什么,如下所示。

@Override
public void put(String key, String value) {httpHeaders.add(key, value);
}

真相大白了,就是HttpHeadersCarrier#put方法其实就是往HTTP请求头里塞键值对嘛,那么如法炮制,在示例代码服务端的TracingFilter中,使用的formatFormat.Builtin.HTTP_HEADERScarrierHttpServletRequestExtractAdapter,所以其实最终会调用到HttpServletRequestExtractAdapter#iterator方法,如下所示。

@Override
public Iterator<Map.Entry<String, String>> iterator() {return new MultivaluedMapFlatIterator<>(headers.entrySet());
}

我感觉我又懂了,这肯定就是外层会for循环HttpServletRequestExtractAdapter,从而HttpServletRequestExtractAdapteriterator() 方法就把HTTP请求头的迭代器返回出去了,然后外层就能拿到从客户端传递过来的数据了,包括SpanContextBaggage中的内容都能拿到,从而完成了跨进程传递链路信息。

现在知道如何跨进程传递链路信息了,但是我们站在服务端的视角来审视一下,服务端收到一个携带链路信息的请求,然后这个请求在送到业务代码前,先经过了TracingFilter,我们在TracingFilter中通过formatcarrier配合,将客户端传递过来的SpanContext获取了出来,然后基于SpanContext创建出来了当前这一个工作单元的Span,那么后续就应该进入到业务代码中了对吧,但是这个热乎乎的Span对象怎么传递下去呢,如果是通过方法参数的形式传递下去,那侵入性可太大了,例如Go语言,可能真的只能把Span放在context里面,然后把context当传家宝一样的逐层转递下去,但若是Java语言,我们应该想到一个很好使的东西,叫做ThreadLocal,没错,Span在线程中的传递,本质是依赖ThreadLocal,我们接着来分析是怎么一回事。

3. Scope和ScopeManager

现在来到示例的服务端TracingFilter的代码,注意到在获取到Span对象后,后续执行了如下这么一行代码。

Scope scope = tracer.activateSpan(span)

好的,目标出现了,上述代码通过TraceractivateSpan() 方法传入Span并得到了一个ScopeSpan我们已经很熟了,但是Scope又是什么妖魔鬼怪呢,我们先看一下Tracer接口中activateSpan() 方法的签名。

Scope activateSpan(Span span);

Tracer接口中的activateSpan() 方法用于在当前线程中激活一个Span,这里激活其实可以理解为绑定,即将一个Span绑定到当前线程上,然后得到一个Scope对象,那么再继续看一下Scope接口长啥样,如下所示。

public interface Scope extends Closeable {@Overridevoid close();}

只有一个close() 方法,这里其实就是反激活一个Span,即将一个Span从当前线程取消绑定,那么到这里,Scope基本可以理解为控制一个Span在线程中的活动时间段,当基于某个Span创建得到一个Scope起,这个Span在当前线程就开始活跃了,也就是当前线程任何一个地方都可以操作这个Span,当调用Scopeclose() 方法时,对应Span就不活跃了,也就是当前线程不再能够任意操作这个Span了。

基本了解Scope的概念后,还是得结合具体的实现来加深理解,我们还是以Jaeger的实现进行举例,看一下JaegerTracer#activateSpan方法的实现,如下所示。

@Override
public Scope activateSpan(Span span) {return scopeManager().activate(span);
}

上述scopeManager() 方法默认情况下会返回一个ThreadLocalScopeManager,先简单将ScopeManager理解为管理Scope的管理器即可,下面先看一下ThreadLocalScopeManager的实现。

public class ThreadLocalScopeManager implements ScopeManager {final ThreadLocal<ThreadLocalScope> tlsScope = new ThreadLocal<ThreadLocalScope>();@Overridepublic Scope activate(Span span) {return new ThreadLocalScope(this, span);}@Overridepublic Span activeSpan() {ThreadLocalScope scope = tlsScope.get();return scope == null ? null : scope.span();}
}

我突然感觉自己就悟了,我们传进来的Span会被封装为一个ThreadLocalScope对象,同时ThreadLocalScopeManager中又有一个类型为ThreadLocal<ThreadLocalScope>的tlsScope字段,所以创建出来的ThreadLocalScope对象肯定就是放在了tlsScope中,这样就完成和当前线程绑定了嘛,但是上面的activate() 方法好像没有看到那里有将创建出来的ThreadLocalScope对象往tlsScope放,所以玄机肯定就在ThreadLocalScope的构造函数中,如下所示。

ThreadLocalScope(ThreadLocalScopeManager scopeManager, Span wrapped) {this.scopeManager = scopeManager;this.wrapped = wrapped;this.toRestore = scopeManager.tlsScope.get();scopeManager.tlsScope.set(this);
}

ThreadLocalScope在构造函数中做了两件事情,其一是将之前激活的Span获取出来以备后续恢复,也就是从ThreadLocal中把之前绑定的Span获取出来保存起,其二是将当前的Span放到ThreadLocal中,这会覆盖之前放的Span,这就表明同一线程同一时刻只能同时激活一个Span

到这里就清晰明了了,我们要激活的Span其实是被包装成了一个ThreadLocalScope对象,然后所谓的激活,就是把ThreadLocalScope对象设置到ThreadLocalScopeManager持有的一个ThreadLocal中。

现在Span是激活了,那么我们要怎么获取到激活的Span呢,这就得看一下TraceractiveSpan() 方法,Tracer接口中activeSpan() 方法的签名如下所示。

Span activeSpan();

直接看一下JaegerTraceractiveSpan() 方法的实现,如下所示。

@Override
public Span activeSpan() {return this.scopeManager.activeSpan();
}

继续跟进ThreadLocalScopeManageractiveSpan() 方法,如下所示。

@Override
public Span activeSpan() {ThreadLocalScope scope = tlsScope.get();return scope == null ? null : scope.span();
}

其实就是从ThreadLocal中把激活的Span获取出来。

最后再来分析一下ThreadLocalScopeclose() 方法,如下所示。

@Override
public void close() {if (scopeManager.tlsScope.get() != this) {return;}scopeManager.tlsScope.set(toRestore);
}

因为每个Span在激活时,都会把之前已经激活的Span保存下来,而如果当前Span对应的ThreadLocalScopeclose() 方法被调用,则表明当前Span已经结束活动,那么此时就应该将保存下来的之前Span再重新与当前线程绑定,所以ThreadLocalScopeclose() 方法干的就是这么一个事情,而且ThreadLocalScopeclose() 方法一定要记得调用,否则会引发内存泄漏。

4. Tracer

其实前文有提到Tracer是什么,之所以在这里又重新分析一次Tracer,是因为只有在知道SpanSpanContextInjectExtract,以及ScopeScopeManager之后,才能明白Tracer是干什么的。在io.opentracing中定义的Tracer接口上,有如下这么一段注释。

Tracer is a simple, thin interface for Span creation and propagation across arbitrary transports.

也就是Tracer主要完成两件事情,其一是创建Span,其二是传递Span

对于创建Span来说,Tracer主要是通过buildSpan() 方法得到一个SpanBuilder,然后基于SpanBuilder可以设置Span的父SpanTags等信息,最后通过SpanBuilderstart() 方法创建出来Span。创建Span的相关源码,在本节第1小节介绍SpanSpanContext时已经进行了分析,这里就不重复分析了。

对于传递Span来说,又分两种情况,其一是跨进程传递,其二是线程中传递。对于跨进程传递,主要就是依赖Tracer的如下两个方法。

<C> void inject(SpanContext spanContext, Format<C> format, C carrier);<C> SpanContext extract(Format<C> format, C carrier);

跨进程传递Span,传递的主体是SpanSpanContextinject() 方法可以把SpanContext注入到跨进程传递的介质中,例如HTTP请求头,具体一点就是利用format配套的Injector,把SpanContext注入给carrier中的传递介质例如HTTP请求头,然后extract() 方法可以从跨进程传递介质中把SpanContext提取出来,具体一点就是利用format配套的Extractor,从carrier中的传递介质例如HTTP请求头中提取出SpanContext。跨进程传递Span的相关源码,在本节第2小节介绍InjectExtract时已经进行了分析,这里就不重复分析了。

对于线程中传递Span,主要是依赖Tracer的如下两个方法。

Scope activateSpan(Span span);Span activeSpan();

activateSpan() 方法意为激活Span,实际就是将Span和当前线程绑定,activeSpan() 方法意为拿到激活的Span,实际也就是拿到和当前线程绑定的Span,所以通过activateSpan() 方法和activeSpan() 方法,就能实现Span在线程中的传递,并且在Java语言中,绑定一个东西到一个线程,天然的适合使用ThreadLocal来使用,所以通常Span就是放在ThreadLocal中进行线程中传递的,具体的源码实现在本节第3小节介绍ScopeScopeManager时已经进行了分析,这里就不重复分析了。

那么至此,Tracer是干啥的,其实是比较清楚的了,称之为Span发射器,不为过吧。

5. Jaeger中的Reporter

可以注意到,Span接口有定义一个finish() 方法,该方法签名如下所示。

void finish();

由上可知finish() 方法主要完成的事情就是设置Span的完成时间并记录Span,至于如何记录,取决于各家实现,本小节主要是分析一下Jaeger中的SpanReporter,首先看一下JaegerSpan#finish方法的实现,如下所示。

@Override
public void finish() {if (computeDurationViaNanoTicks) {long nanoDuration = tracer.clock().currentNanoTicks() - startTimeNanoTicks;finishWithDuration(nanoDuration / 1000);} else {finish(tracer.clock().currentTimeMicros());}
}

Span的持续时间就等于当前时间减去startTimeNanoTicksstartTimeNanoTicks是在Span被创建的时候完成赋值的,现在继续跟进一下JaegerSpan#finishWithDuration方法,如下所示。

private void finishWithDuration(long durationMicros) {if (durationMicrosUpdater.compareAndSet(this, null, durationMicros)) {if (context.isSampled()) {tracer.reportSpan(this);}} else {log.warn("Span has already been finished; will not be reported again.");}
}

上述方法实际就是基于CAS的方式来更新JaegerSpandurationMicroseconds字段,更新成功则调用TracerreportSpan() 方法来记录Span,更新失败则表示Span已经被完成过,而Span是不能重复被完成的。现在继续看一下JaegerTracer#reportSpan方法的实现,如下所示。

void reportSpan(JaegerSpan span) {reporter.report(span);metrics.spansFinished.inc(1);
}

上述方法中会使用JaegerTracer持有的reporter字段(类型是Reporter)来记录Span,而JaegerTracer持有的reporter是在创建JaegerTracer时设置的,回顾一下示例代码中的TracerConfig,如下所示。

@Configuration
public class TracerConfig {@Autowiredprivate SpanReporter spanReporter;@Autowiredprivate Sampler sampler;@Beanpublic Tracer tracer() {return new JaegerTracer.Builder(TRACER_SERVICE_NAME).withTraceId128Bit().withSampler(sampler).withReporter(spanReporter).build();}}

我们在创建JaegerTracer时,设置了一个我们自己写的SpanReporterJaegerTracer,我们自己写的SpanReporter实现了Reporter接口,如下所示。

public class SpanReporter implements Reporter {public void report(JaegerSpan span) {}public void close() {}}

所以在最终记录Span时,每一个Span都会送到我们写的SpanReporter中来,那这个时候所谓的记录Span,就很有可操作性了。

已知一条分布式链路中,实际就是由一个又一个的Span串起来的,每个Span都有一个相同的traceId,表明大家都是同一个分布式链路上的工作单元,同时每个Span都有spanIdparentSpanId,这样可以通过某个SpanspanIdparentSpanId明确这个Span在这条分布式链路中所处的位置,所以我们在记录Span时,其实就是需要读取出SpantraceIdspanIdparentSpanIddurationMicroseconds等参数,然后将这些参数组装成一定格式的数据,最后将Span的格式化后的数据送到分布式链路追踪系统的服务端,后续分布式链路追踪系统的服务端就可以结合指定的格式,解析出一个又一个Span的链路信息,最终给用户呈现出一条又一条的分布式链路。

我们在示例的SpanReporter中,没有对Span做任何操作,这主要是为了简化示例代码,但实际上,我们可以通过各种方式来发送Span数据给到我们的分布式链路追踪系统的服务端,例如发送Kafka,再例如打印日志再进行日志采集,总之可操作性是很强的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/3016655.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

QT--2

Qt界面设计 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent) {//窗口相关设置this->resize(680,520);this->setFixedSize(680,520);this->setWindowTitle("Tim");this->setWindowFla…

在拥有多个同名称密码的ap环境中,如何连接到指定信道或mac的ap路由器?

在给客户做ESP32-C3入墙开关项目时&#xff0c;客户问&#xff1a;在拥有多个同名称密码的ap环境中&#xff0c;如何连接到指定信道或mac的ap路由器&#xff1f;针对这个问题&#xff0c;启明云端工程师给出下面解决方法。 1、将wifi_sta_config_t配置中的channel配置为该信道…

Codeforces Round 942 (Div. 2) A-D1

题目&#xff1a; Codeforces Round 942 (Div. 2) D2有缘再补吧… A. Contest Proposal 题意 两个升序&#xff08;不降&#xff09;的序列a和b&#xff0c;可以在a的任意位置插入任意数&#xff08;要保持升序&#xff09;&#xff0c;使对任意i&#xff0c;有a[i] < b[…

js 图片渐变

1. 点击图片&#xff0c;使其渐变为另一张图片 通过定义keyframes来创建一个淡入淡出的动画效果。当图片被点击时&#xff0c;先添加淡出动画使图片透明度从0渐变到1&#xff0c;然后在1秒后切换图片源并添加淡入动画使新图片透明度从0渐变到1&#xff0c;实现图片渐变效果。 …

Django调用MTP服务器给指定邮箱发送邮件

Django调用MTP服务器发送邮箱 邮箱的激活链接含有用户数据不能直接发送需要对其进行加密 发送邮箱是借助SMTP服务器进行中转 一. 配置SMTP服务中的邮箱信息以及激活链接 1. 配置邮箱权限 打开网易邮箱设置点击POP3 开启选项 注 : 在打开的过程中会弹出授权密码一点要保存 …

【Unity动画系统】动画层级(Animation Layer)讲解与使用

如何使用Unity的Animation Layer和Avater Mask把多个动画组合使用 想让玩家持枪行走&#xff0c;但是手里只有行走和持枪站立的动作。 Unity中最方便的解决办法就是使用动画层级animation layer以及替身蒙版avatar mask。 创建一个动画层级 Weight表示权重&#xff0c;0的话则…

1.2 Java全栈开发前端+后端(全栈工程师进阶之路)-前置课程JavaScript,看这一篇就够了

上一章我们讲了HTML的知识&#xff0c;那么我们现在要来学习&#xff0c;JavaScript 那么首先我们要知道JavaScript写在哪里。 JavaScript核心语法 js书写的位置 1、写在页面中的script标签下 只有在代码与页面有强关联的情况下才会写在页面里 2、写在指定的js文件下通过…

下一代Nginx? OpenNjet 的入门实践

何为 OpenNjet &#xff1f; OpenNJet 应用引擎是基于 NGINX 的面向互联网和云原生应用提供的运行时组态服务程序&#xff0c;作为底层引擎&#xff0c;OpenNJet 实现了NGINX 云原生功能增强、安全加固和代码重构&#xff0c;利用动态加载机制可以实现不同的产品形态&#xff0…

Python基础详解二

一&#xff0c;函数 函数是组织好的&#xff0c;可重复使用的&#xff0c;用来实现某个功能的代码段 def myMethod(data):print("数据长度为",len(data))myMethod("dsdsdsds") 函数的定义&#xff1a; def 函数名(传入参数):函数体return 返回值 def m…

C++笔记-makefile添加第三方.h和.cpp及添加.h和lib库模板

目文件结构如下所示时&#xff1a; project/├── main.cpp├── test.cpp├── DIRA/│ ├── A.cpp│ └── A.h├── DIRBLIB/│ └── libB.so└── include/└── B.h Makefile如下所示&#xff1a; # 编译器设置 CXX g CXXFLAGS -stdc11 -Wall# 目录…

Linux 进程间通信之共享内存

&#x1f493;博主CSDN主页:麻辣韭菜&#x1f493;   ⏩专栏分类&#xff1a;Linux知识分享⏪   &#x1f69a;代码仓库:Linux代码练习&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习更多Linux知识   &#x1f51d; ​ 目录 ​编辑​ 前言 共享内存直接原理…

C语言栈的含义与栈数据操作代码详解!

引言&#xff1a;在本篇博客中&#xff0c;我们将学到数据结构——栈&#xff0c;讲到栈的含义与关于栈的数据操作代码。栈可以在顺序表、双向链表以及单链表的基础上实现&#xff0c;而于本篇博客中&#xff0c;我们选择在顺序表的基础上实现栈。 更多有关C语言和数据结构知识…

Ansible自动化运维工具 - playbook 剧本编写

一. inventory 主机清单 Inventory 支持对主机进行分组&#xff0c;每个组内可以定义多个主机&#xff0c;每个主机都可以定义在任何一个或多个主机组内。 1.1 inventory 中的变量含义 Inventory 变量名 含义ansible_hostansible连接节点时的IP地址ansible_port连接对方…

CANdela/Diva系列1--CANdela Studio的基本介绍

大家好&#xff0c;这个系列主要给大家介绍跟诊断相关的Vector 工具CANdela和Diva&#xff0c;首先介绍CANdela。 目录 1.CANdela的简介&#xff1a; 2.如何打开CANdela 工程&#xff1a; 3.CANdela工程的详细介绍&#xff1a; 3.1 工具栏的介绍&#xff1a; 3.2 工作树的…

2024-05-07 商业分析-赚钱之前怎么预估风险-记录

摘要: 2024-05-07 商业分析-赚钱之前怎么预估风险-记录 赚钱之前怎么预估风险 好&#xff0c;大家好&#xff0c;今天我们来讲的是叫什么赚钱之前怎么评估风险啊&#xff0c;这个问题啊提的很好啊&#xff0c;我们待会儿来讲啊。呃&#xff0c;首先呢今天这个主题呢来自于昨天…

Python实现txt转Excel(坐标)

import pandas as pddef txt_to_excel(txt_file, excel_file):# 读取 txt 文件with open(txt_file, r) as f:lines f.readlines()# 将每行数据分割成多个单元格data []for line in lines:row line.strip().split( )data.append(row)# 将数据保存到 Excel 文件df pd.DataFra…

剧本杀小程序,为商家带来更多收益

剧本杀作为一种社交类游戏&#xff0c;关注度越来越高&#xff0c;目前&#xff0c;市场上剧本杀依然呈现上升发展趋势。 不过当下&#xff0c;在剧本杀市场中&#xff0c;大部分商家都开始使用小程序管理运营剧本杀。相对于线下剧本杀&#xff0c;线上剧本杀小程序便于商家管…

一起深度学习

CIFAR-10 卷积神经网络 下载数据集构建网络运行测试 下载数据集 batchsz 32cifar_train datasets.CIFAR10(data,trainTrue,transformtorchvision.transforms.Compose([torchvision.transforms.Resize((32,32)),torchvision.transforms.ToTensor()]),downloadTrue)cifar_train …

基于FPGA的DDS波形发生器VHDL代码Quartus仿真

名称&#xff1a;基于FPGA的DDS波形发生器VHDL代码Quartus仿真&#xff08;文末获取&#xff09; 软件&#xff1a;Quartus 语言&#xff1a;VHDL 代码功能&#xff1a; DDS波形发生器VHDL 1、可以输出正弦波、方波、三角波 2、可以控制输出波形的频率 DDS波形发生器原理…

什么是光伏发电?什么是分布式光伏系统?

一、光伏发电 光伏发电&#xff0c;作为一种可再生能源利用技术&#xff0c;其核心原理基于半导体的光生伏特效应。简而言之&#xff0c;光伏发电就是将太阳能直接转换为电能的过程。它由三个主要部分组成&#xff1a;太阳电池板&#xff08;组件&#xff09;、控制器和逆变器…