使用 Instrumenter API

Instrumenter封装用于收集遥测,从收集的数据,以起始和结束跨度,使用度量仪器记录的值的整个逻辑。Instrumenter 公共API只包含三个方法:shouldStart()start()end()。该类旨在装饰检测库代码的实际调用;shouldStart()start()方法在请求处理开始的时候调用。end()方法必须在请求处理结束和响应到达或者因错误而失败时调用。该Instrumenter是参数化的泛型类REQUESTRESPONSE类型。它们代表着操作的输入和输出。Instrumenter可以配置各种提取器,可以增强或修改遥测数据。

使用 shouldStart()来检查当前操作是否生成了任何遥测数据。

第一个方法,需要在任何其他Instrumenter方法之前调用shouldStart().它确定是否应该对操作进行遥测检测。该Instrumenter框架实现了几个抑制规则,以防止生成重复的遥测数据;例如,相同的 HTTP 服务器请求总是产生单个 HTTPSERVER跨度。

shouldStart()方法接受当前的 OpenTelemetryContext和检测的库REQUEST类型。参考以下示例:

  1. Response decoratedMethod(Request request) {
  2. Context parentContext = Context.current();
  3. if (!instrumenter.shouldStart(parentContext, request)) {
  4. return actualMethod(request);
  5. }
  6. // ...
  7. }

如果该shouldStart()方法返回false,则剩余的Instrumenter方法不应该被调用。

开始使用start()启动检测操作

shouldStart()返回时true,您可以使用start()来启动检测操作。 该start()方法开始收集有关正在调用的检测库函数的遥测数据。它启动Span并开始记录指标(如果在使用的Instrumenter实例中注册了任何指标)。

start()方法接受当前的 OpenTelemetryContext和检测的库REQUEST类型,并返回新的 OpenTelemetry Context,它应该在检测操作结束之前变为当前状态。参考以下示例:

  1. Response decoratedMethod(Request request) {
  2. // ...
  3. Context context = instrumenter.start(parentContext, request);
  4. try (Scope scope = context.makeCurrent()) {
  5. // ...
  6. }
  7. }

新启动一个context作为当前的上下文,并在其内部scope调用实际的库方法。

使用 end()结束检测操作

end()当检测操作完成时调用该方法。始终在 之后调用此方法非常重要start()start()不稍后调用end() 将导致不准确或错误的遥测和上下文泄漏。该end()方法结束当前跨度并完成记录指标(如果在Instrumenter实例中注册了任何指标)。

end()方法接受几个参数:

  • start()方法返回一个OpenTelemetry的Context
  • REQUEST开始处理的实例。
  • 可选地,RESPONSE结束处理的实例 - 可能是null在未收到或发生错误的情况下。
  • (可选)使用Throwable操作引发的错误。

参考以下示例:

  1. Response decoratedMethod(Request request) {
  2. Context parentContext = Context.current();
  3. if (!instrumenter.shouldStart(parentContext, request)) {
  4. return actualMethod(request);
  5. }
  6. Context context = instrumenter.start(parentContext, request);
  7. try (Scope scope = context.makeCurrent()) {
  8. Response response = actualMethod(request);
  9. instrumenter.end(context, request, response, null);
  10. return response;
  11. } catch (Throwable error) {
  12. instrumenter.end(context, request, null, error);
  13. throw error;
  14. }
  15. }

在代码示例中start()方法返回的值context被传递给end() 方法。end()无论装饰的结果如何,无论actualMethod()是有效响应还是错误,始终调用该方法。

使用InstrumenterBuilder创建一个新的 Instrumenter

一个Instrumenter可以通过调用它的静态获得builder()方法和使用返回的InstrumenterBuilder配置捕获遥测和应用自定义设置。该builder()方法接受三个参数:

  • 一个OpenTelemetry实例,用于获取TracerMeter对象。
  • 检测名称,指示检测库名称,而不是 检测库名称。此处传递的值应唯一标识检测库,以便在故障排除期间可以确定遥测数据的来源。在OpenTelemetry 规范中阅读有关检测库的更多信息。
  • SpanNameExtractor确定span名称。

一个Instrumenter可以由几个较小的组件构建。以下小节描述了可用于自定义Instrumenter.

使用 SpanNameExtractor命名span

SpanNameExtractor是一个简单的函数式接口,它接受REQUEST类型并返回跨度名称。有关跨度命名的更详细指南,请查看Span规范 和跟踪语义约定

参考以下示例:

  1. class MySpanNameExtractor implements SpanNameExtractor<Request> {
  2. @Override
  3. public String extract(Request request) {
  4. return request.getOperationName();
  5. }
  6. }

示例SpanNameExtractor实现采用请求类型提供的拟合值并将其用作span名称。请注意,这SpanNameExtractor是一个@FunctionalInterface: 而不是将它作为一个单独的类来实现,您可以只传递Request::getOperationName给该builder() 方法。

使用AttributesExtractor给span和metric数据添加属性

AttributesExtractor负责在处理开始和结束时提取span和metric属性。它包含两种方法:

  • onStart()当检测操作开始时调用该方法。它接受两个参数:一个AttributesBuilder实例和传入的REQUEST实例。
  • onEnd()当检测操作结束时调用该方法。它接受与相同的两个参数onStart()以及一个可选RESPONSE和一个可选的Throwable错误。

这两种方法的目的都是从接收到的请求(以及响应或错误)中提取感兴趣的属性并将它们设置到构建器中。一般来说,最好在onStart()填充attributes,因为这些attributes可用于Sampler.

参考以下示例:

  1. class MyAttributesExtractor implements AttributesExtractor<Request, Response> {
  2. private static final AttributeKey<String> NAME = stringKey("mylib.name");
  3. private static final AttributeKey<Long> COUNT = longKey("mylib.count");
  4. @Override
  5. public void onStart(AttributesBuilder attributes, Request request) {
  6. set(attributes, NAME, request.getName());
  7. }
  8. @Override
  9. public void onEnd(
  10. AttributesBuilder attributes,
  11. Request request,
  12. @Nullable Response response,
  13. @Nullable Throwable error) {
  14. if (response != null) {
  15. set(attributes, COUNT, response.getCount());
  16. }
  17. }
  18. }

上面的AttributesExtractor示例实现设置了两个属性:一个从请求中提取,一个从响应中提取。建议将AttributeKey实例保留为静态final常量并重用它们。每次设置属性时创建一个新键都有引入不必要开销的风险。

您可以使用或方法AttributesExtractor向中添加。InstrumenterBuilder``addAttributesExtractor()``addAttributesExtractors()

您也可以使用addAttributesExtractor()或者addAttributesExtractors()InstrumenterBuilder添加一个AttributesExtractor

使用 SpanStatusExtractor 设置span状态

默认情况下,当Throwable发生错误的时候设置span状态为StatusCode.ERROR,其他情况使用StatusCode.UNSET。设置自定义SpanStatusExtractor允许自定义此行为。

SpanStatusExtractor接口只有一个方法extract()接受REQUEST,一个可选RESPONSE和一个可选的Throwable错误。它应该返回一个StatusCode将在span包装检测操作结束时设置的。

参考以下示例:

  1. class MySpanStatusExtractor implements SpanStatusExtractor<Request, Response> {
  2. @Override
  3. public StatusCode extract(
  4. Request request,
  5. @Nullable Response response,
  6. @Nullable Throwable error) {
  7. if (response != null) {
  8. return response.isSuccessful() ? StatusCode.OK : StatusCode.ERROR;
  9. }
  10. return SpanStatusExtractor.getDefault().extract(request, response, error);
  11. }
  12. }

上面SpanStatusExtractor的示例根据操作结果返回一个自定义StatusCode,编码在response类中。如果响应不存在(例如,由于错误),使用SpanStatusExtractor.getDefault()方法返回默认的行为。

您可以使用setSpanStatusExtractor()方法在InstrumenterBuilder 设置SpanStatusExtractor

使用 SpanLinksExtractor添加span 关联

SpanLinksExtractor接口可用于在检测操作开始时添加到其他span的链接。它有一个extract()接收以下参数的方法:

  • 一个SpanLinkBuilder可以用来添加的链接。
  • 通过Instrumenter.start()传递父Context
  • 通过Instrumenter.start()传递REQUEST

您可以在此处阅读有关跨度链接及其用例的更多信息。

参考以下示例:

  1. class MySpanLinksExtractor implements SpanLinksExtractor<Request> {
  2. @Override
  3. public void extract(SpanLinksBuilder spanLinks, Context parentContext, Request request) {
  4. for (RelatedOperation op : request.getRelatedOperations()) {
  5. spanLinks.addLink(op.getSpanContext());
  6. }
  7. }
  8. }

SpanLinksExtractor示例中,请求对象很方便的使用getRelatedOperations()方法查找所有的span并链接到一起。当这样的函数不存在时,您需要从请求头或者元数据中提取需要构造的信息来构造一个SpanContext

您可以使用addSpanLinksExtractor()方法在InstrumenterBuilder 设置SpanLinksExtractor

使用ErrorCauseExtractor丢弃包装异常类型

发生错误时,根本原因可能隐藏在几个“包装器”异常类型后面,例如来自 Java 标准库的ExecutionExceptionCompletionException。默认情况下,来自 JDK 的已知包装器异常类型将从捕获的错误中删除。要删除其他包装器异常,例如检测库提供的异常,您可以实现ErrorCauseExtractor,它具有以下功能:

  • 它只有extractCause()一种方法负责剥离不必要的异常层并提取导致操作失败的实际错误。
  • 它接受一个Throwable并返回一个Throwable

参考以下示例:

  1. class MyErrorCauseExtractor implements ErrorCauseExtractor {
  2. @Override
  3. public Throwable extractCause(Throwable error) {
  4. if (error instanceof MyLibWrapperException && error.getCause() != null) {
  5. error = error.getCause();
  6. }
  7. return ErrorCauseExtractor.jdk().extractCause(error);
  8. }
  9. }

ErrorCauseExtractor示例中,实现检查错误是否是MyLibWrapperException实例并获取原因,在这种情况下它会解开它。error.getCause() != null检查非常重要:如果提取器没有验证这两个条件,就可能意外地删除整个异常,使检测丢失一个异常错误,从而从根本上改变了捕获的遥测。接下来,提取器返回到jdk()删除已知 JDK 包装器异常类型的默认实现。

您可以使用setErrorCauseExtractor()方法在InstrumenterBuilder 设置ErrorCauseExtractor

使用TimeExtractor自定义操作开始时间和结束时间

在某些情况下,检测库提供了一种方法来检索操作开始和结束的准确时间戳。TimeExtractor接口可用于将此信息提供给 OpenTelemetry trace和metric数据。

extractStartTime()只能从请求中提取时间戳。extractEndTime() 接受请求、可选响应和可选Throwable错误。

参考以下示例:

  1. class MyTimeExtractor implements TimeExtractor<Request, Response> {
  2. @Override
  3. public Instant extractStartTime(Request request) {
  4. return request.startTimestamp();
  5. }
  6. @Override
  7. public Instant extractEndTime(Request request, @Nullable Response response, @Nullable Throwable error) {
  8. if (response != null) {
  9. return response.endTimestamp();
  10. }
  11. return request.clock().now();
  12. }
  13. }

上面的示例实现使用请求来检索开始时间戳。如果可用,则响应用于计算结束时间;如果它丢失(例如,发生错误时),则使用相同的时间源来计算当前时间戳。

您可以在InstrumenterBuilder使用setTimeExtractor() 方法中设置时间提取器。

通过实现RequestMetricsRequestListener注册 metrics

如果您需要向Instrumenter中添加指标,您可以实现RequestMetricsRequestListener接口。RequestMetrics只是一个工厂接口RequestListener- 它接收一个 OpenTelemetryMeter并返回一个新的监听器。

RequestListener包含两个方法:

  • start()在检测操作开始时执行。它返回一个Context-如果有需要的话,它可以使用内部存储指标状态并传递到end()
  • end() 在检测操作结束时执行。

这两种方法都接收一个Context,它的一个实例Attributes包含在检测操作开始或结束时计算的属性,以及可用于准确计算持续时间的开始和结束纳秒时间戳。

参考以下示例:

  1. class MyRequestMetrics implements RequestListener {
  2. static RequestMetrics get() {
  3. return MyRequestMetrics::new;
  4. }
  5. private final LongUpDownCounter activeRequests;
  6. MyRequestMetrics(Meter meter) {
  7. activeRequests = meter
  8. .upDownCounterBuilder("mylib.active_requests")
  9. .setUnit("requests")
  10. .build();
  11. }
  12. @Override
  13. public Context start(Context context, Attributes startAttributes, long startNanos) {
  14. activeRequests.add(1, startAttributes);
  15. return context.with(new MyMetricsState(startAttributes));
  16. }
  17. @Override
  18. public void end(Context context, Attributes endAttributes, long endNanos) {
  19. MyMetricsState state = MyMetricsState.get(context);
  20. activeRequests.add(1, state.startAttributes());
  21. }
  22. }

上面实例列出了通过静态方法get()实现一个RequestMetrics工厂接口。监听器使用计数器实现了当前正在请求的数量。请注意,start()end()方法之间的状态是使用MyMetricsState类(一个非常简单的数据类,未在上面的示例中列出)共享的,使用Context.

上面列出的示例类RequestMetrics在静态get()方法中实现了工厂接口。监听器实现使用计数器来测量当前正在传输的请求数。请注意,start()end()方法之间的状态是使用MyMetricsState类(一个非常简单的数据类,未在上面的示例中列出)共享的,使用Context进行传递。

您可以添加RequestMetricsInstrumenterBuilderusingaddRequestMetrics()方法。

您可以在InstrumenterBuilder使用addRequestMetrics() 方法来添加RequestMetrics

使用ContextCustomizerContext提供丰富的操作

在一些罕见的情况下,需要Context在从Instrumenter#start()方法返回之前填充它。该ContextCustomizer接口可用于实现这一点。它暴露了一个start()方法并ContextREQUESTAttributes参数,在操作开始时提取的方法,并返回一个修改后的Context.

参考以下示例:

  1. class MyContextCustomizer implements ContextCustomizer<Request> {
  2. @Override
  3. public Context start(Context context, Request request, Attributes startAttributes) {
  4. return context.with(new InProcessingAttributesHolder());
  5. }
  6. }

示例ContextCustomizer上方插入一个额外的InProcessingAttributesHolderContext并在Instrumenter#start()方法之前将其从返回。在InProcessingAttributesHolder类,正如它的名字所暗示的,可以用来跟踪不可用的要求开始或结束的属性-例如,如果用仪器库只在加工过程中暴露的重要信息。可以从当前查找持有者类,Context并在检测操作开始和结束之间填充该信息。它可以稍后作为RESPONSE类型(或其一部分)传递给Instrumenter#end()方法,以便配置AttributesExtractor可以处理收集的信息并将其转换为遥测属性。

您可以在InstrumenterBuilder使用addContextCustomizer() 方法来添加ContextCustomizer

禁用检测

在极少数情况下,完全禁用构造Instrumenter可能很有用,例如,基于配置属性。InstrumenterBuilder暴露了setDisabled() 方法:通过true将转向新创建Instrumenter成无操作实例。

最后,通过 SpanKindExtractor 设置span类型并获取一个新的 Instrumenter!

使用InstrumenterBuilder以下的一种方法来使Instrumenter创建结束:

  • newInstrumenter():返回的Instrumenter将始终以 kind 开头INTERNAL
  • newInstrumenter(SpanKindExtractor):返回的Instrumenter将始终以传递的SpanKindExtractor.
  • newClientInstrumenter(TextMapSetter):返回的Instrumenter将始终启动CLIENT 跨度并将操作上下文传播到传出请求中。
  • newServerInstrumenter(TextMapGetter):返回的Instrumenter将始终启动SERVER 跨度,并将从传入请求中提取父跨度上下文。
  • newProducerInstrumenter(TextMapSetter):返回的Instrumenter将始终启动PRODUCER 跨度并将操作上下文传播到传出请求中。
  • newConsumerInstrumenter(TextMapGetter):返回的Instrumenter将始终启动SERVER 跨度,并将从传入请求中提取父跨度上下文。

最后4个变种用来创建非INTERNAL span,它接收TextMapSetterTextMapGetter实现作为参数。这些是正确实现服务之间的上下文传播所必需的。如果您想了解如何使用和实现这些接口,请阅读OpenTelemetry Java 文档

SpanKindExtractor由上面列表中的第二个变种接收的接口是一个简单的接口,它接收一个REQUEST并返回 SpanKind,该接口应在启动此操作的span时使用。

参考以下示例:

  1. class MySpanKindExtractor implements SpanKindExtractor<Request> {
  2. @Override
  3. public SpanKind extract(Request request) {
  4. return request.shouldSynchronouslyWaitForResponse() ? SpanKind.CLIENT : SpanKind.PRODUCER;
  5. }
  6. }

实例SpanKindExtractor根据请求处理决定是否使用PRODUCERCLIENT。此示例反映了一个真实场景:您可能会在消息传递库检测中找到类似的代码,因为根据OpenTelemetry 消息传递语义约定 ,如果发送消息是完全同步的并等待响应,则span 类型设置为CLIENT