学习如何为你的库添加本地工具

OpenTelemetry为许多库提供[仪表库][],通常是通过库钩子或猴子补丁库代码来完成的。

使用OpenTelemetry进行本地库工具提供更好的可观测性和开发人员体验,无需库来暴露和文档化钩子:

  • 可以用常见且易于使用的OpenTelemetry API替换自定义日志钩子,用户只与OpenTelemetry交互
  • 来自库和应用代码的跟踪,日志和指标是相关且一致的
  • 通用约定使用户可以在相同技术中和跨库和语言中获得类似和一致的遥测
  • 使用广泛的OpenTelemetry扩展点,可以为各种消费情景微调(过滤、处理、聚合)遥测信号,这些扩展点都有很好的文档说明。

语义约定

查看可用的语义约定,涵盖了Web框架、RPC客户端、数据库、消息客户端、基础设施等。

如果你的库是其中之一,请遵循这些约定,它们是主要的真实来源,并告知要在跨度中包含哪些信息。遵循约定可以使工具仪表盘独立于技术构建并为各种技术(如数据库或消息系统)提供支持。当库遵循约定时,很多场景可以在没有用户输入或配置的情况下启用。

语义约定始终在不断发展,并且不断添加新的约定。如果某个约定对于你的库尚不存在,请考虑添加它们。在定义它们时要特别注意跨度名称;努力使用有意义的名称,并考虑将其定义为一个序列。

有一个可以用于记录所使用的语义约定版本的schema_url属性。请尽量设置此属性。

如果你有任何反馈意见或想要添加新的约定,请参与贡献!可以从 Instrumentation SlackSpecification repository开始!

定义跨度

从库使用者的角度考虑你的库以及用户可能对库的行为和活动感兴趣的内容。作为库的维护者,你了解内部工作原理,但用户很可能对库的内部工作原理不太感兴趣,而更关注其应用功能。思考哪些信息对于分析库的使用可能有帮助,然后考虑适当的数据建模方式。以下是一些需要考虑的内容:

  • 跨度和跨度层次结构
  • 跨度上的数字属性(作为聚合指标的替代方案)
  • 跨度事件
  • 聚合指标

例如,如果你的库向数据库发出请求,仅为数据库的逻辑请求创建跨度。与数据库实现该功能的库中进行网络请求的物理请求应进行仪表化。你还应优先考虑捕获其他活动,例如对象/数据序列化作为跨度事件,而不是额外的跨度。

设置跨度属性时,请遵循语义约定。

进行仪表化的时机

有些库是封装网络调用的薄客户端。很可能OpenTelemetry对底层RPC客户端有一个仪表化库(请查看注册表)。在这种情况下,可能不需要对封装库进行仪表化。作为一般准则,只在库的层次上进行仪表化。

如果符合以下条件,则不用进行仪表化:

  • 你的库是在文档化或自我解释的API之上的薄代理
  • 并且 OpenTelemetry对底层网络调用进行了仪表化
  • 并且 你的库没有任何应遵循的约定来丰富遥测

如果犹豫不决,请不要进行仪表化-当你看到有需要时,你总是可以之后再进行。

如果你选择不进行仪表化,则为提供一种配置OpenTelemetry处理程序的方式仍然很有用。对于不支持完全自动仪表化的语言来说,这是至关重要的,并且对于其他语言也很有用。

本文档的其余部分将指导你在决定要进行仪表化时应该仪表化什么和如何仪表化方面提供指导。

OpenTelemetry API

第一步是依赖OpenTelemetry API包。

OpenTelemetry有两个主要模块 - API和SDK。OpenTelemetry API是一组抽象和非操作实现。除非应用程序导入OpenTelemetry SDK,否则你的仪表化操作什么都不做,也不会影响应用程序性能。

库应该仅使用OpenTelemetry API

在添加新的依赖时,有一些考虑因素可以帮助你决定如何尽量减少依赖关系:

  • OpenTelemetry Trace API在2021年初达到了稳定性,在发布时遵循语义化版本2.0和我们非常重视API的稳定性。

  • 在添加依赖项时,使用最早稳定的OpenTelemetry API版本(1.0.*),避免更新它,除非你必须使用新功能。

  • 在你的仪表化稳定之前,可以考虑将其作为一个单独的包进行发布,这样其他未使用该仪表化的用户就不会受到影响。你可以将其保留在你的存储库中,或者将其添加到 OpenTelemetry,这样它就会与其他仪表化包一起发布。

  • 语义约定是稳定的,但可能会变化:虽然这不会导致任何功能性问题,但你可能需要不时更新你的仪表化。将其放在预览插件中或者放在OpenTelemetry贡献存储库中,可能有助于保持约定的最新状态,而无需对用户进行任何破坏性的更改。

获取跟踪器

通过调用全局TracerProvider提供者,库可以从全局TracerProvider获取跟踪器。

private static final Tracer tracer = GlobalOpenTelemetry.getTracer("demo-db-client", "0.1.0-beta1");

对于库来说,拥有一个允许应用程序显式传递TracerProvider实例的API非常有用,这可以实现更好的依赖注入,并简化测试。

在获取跟踪器时,请提供库(或跟踪插件)的名称和版本-它们会显示在遥测中,并帮助用户处理和筛选遥测,了解它们的来源,并调试/报告任何仪表化问题。

什么要进行仪表化

公共API

公共API是跟踪的良好候选对象:为公共API调用创建的跨度可以帮助用户将遥测映射到应用程序代码,了解库调用的持续时间和结果。应该跟踪哪些调用:

  • 内部进行网络调用的公共方法或执行需要时间并且可能失败的本地操作(例如IO)
  • 处理请求或消息的处理程序

仪表化示例:

private static final Tracer tracer = GlobalOpenTelemetry.getTracer("demo-db-client", "0.1.0-beta1");

private Response selectWithTracing(Query query) {
    // 请查看约定以了解跨度名称和属性的指导
    Span span = tracer.spanBuilder(String.format("SELECT %s.%s", dbName, collectionName))
            .setSpanKind(SpanKind.CLIENT)
            .setAttribute("db.name", dbName)
            ...
            .startSpan();

    // 使跨度活动,并允许关联日志和嵌套跨度
    try (Scope unused = span.makeCurrent()) {
        Response response = query.runWithRetries();
        if (response.isSuccessful()) {
            span.setStatus(StatusCode.OK);
        }

        if (span.isRecording()) {
           // 填充响应代码和其他信息的响应属性
        }
    } catch (Exception e) {
        span.recordException(e);
        span.setStatus(StatusCode.ERROR, e.getClass().getSimpleName());
        throw e;
    } finally {
        span.end();
    }
}

按照约定来填充属性!如果没有适用的属性,请查看一般约定

嵌套网络和其他跨度

网络调用通常通过相应的客户端实现与OpenTelemetry自动仪表化进行跟踪。

Jaeger UI中的嵌套数据库和HTTP跨度

如果OpenTelemetry不支持跟踪你的网络客户端,那么不同情况下的最佳选择是:

  • 跟踪网络调用是否能够提高用户的可观察性或你对其的支持能力?
  • 你的库是否是在公开的、文档化的RPC API之上的封装器?在出现问题时,用户是否需要从底层服务获取支持?
    • 仪表化库并确保跟踪个别网络尝试
  • 使用跟踪这些调用的跨度是否非常冗长?或者它们是否会明显影响性能?
    • 使用具有详细程度的日志或跨度事件:日志可以与父跨度进行关联,而跨度事件应设置在公共API跨度上。
    • 如果它们必须是跨度(以携带和传播唯一的跟踪上下文),请将它们放在配置选项后面,并默认禁用。

如果OpenTelemetry已经支持跟踪你的网络调用,你可能不希望重复进行仪表化。但也有一些例外情况:

  • 支持不使用自动仪表化的用户(在某些环境中可能无法工作或用户可能对猴子补丁存在担忧)
  • 启用与底层服务的自定义(遗留)关联和上下文传播协议
  • 用绝对必要的库/服务特定信息丰富RPC跨度,这些信息不在自动仪表化之内

警告:避免重复的通用解决方案正在施工中🚧。

事件

跨度是应用程序发出的一种信号。事件(或日志)和跨度是相辅相成的,而不是重复的。每当你有一些应该有定性的内容时,日志是比跨度更好的选择。

很可能你的应用程序已经使用了日志记录或类似的模块。你的模块可能已经具有OpenTelemetry集成-要找出,请参阅注册表。集成通常会在所有日志上标记活动跟踪上下文,以便用户可以将它们进行关联。

如果你的语言和生态系统没有常用的日志支持,则可以使用[跨度事件][]来共享其他应用程序详细信息。如果你还想添加属性,事件可能更方便些。

作为经验法则,对于冗长数据使用事件或日志而不是跨度。始终将事件附加到工具生成的跨度实例上,如果可以的话,要避免使用活动跨度,因为你无法控制它所指的是什么。

上下文传播

提取上下文

如果你在处理从上游调用而来的请求或消息(例如Web框架或消息消费者),你应该从传入的请求/消息中提取上下文。OpenTelemetry提供了Propagator API,它隐藏了具体的传播标准,并从网络中读取跟踪Context。在单个响应的情况下,只有一个上下文在网络中,它成为库创建的新跨度的父跨度。

在创建跨度后,你应该将新的跟踪上下文传递给应用程序代码(回调或处理程序),通过使跨度活动来完成;如果可能,请明确地执行此操作。

// 提取上下文
Context extractedContext = propagator.extract(Context.current(), httpExchange, getter);
Span span = tracer.spanBuilder("receive")
            .setSpanKind(SpanKind.SERVER)
            .setParent(extractedContext)
            .startSpan();

// 使跨度活动,以便关联任何嵌套遥测
try (Scope unused = span.makeCurrent()) {
  userCode();
} catch (Exception e) {
  span.recordException(e);
  span.setStatus(StatusCode.ERROR);
  throw e;
} finally {
  span.end();
}

请参阅你所使用语言的OpenTelemetry文档,了解完整的 Java中提取消息 示例。

在消息系统的情况下,你可能一次接收多个消息。接收到的消息将成为创建的跨度上的链接。有关详情,请参阅 传递消息约定(警告:传递消息约定 正在施工中🚧)。

注入上下文

当你发出调用时,通常希望将上下文传播到下游服务。在这种情况下,你应该创建一个新的跨度来跟踪出站调用,并使用Propagator API将上下文注入到消息中。在创建消息进行异步处理时,可能还有其他情况需要注入上下文。

Span span = tracer.spanBuilder("send")
            .setSpanKind(SpanKind.CLIENT)
            .startSpan();

// 使跨度活动,以便关联任何嵌套遥测-即使网络调用可能具有嵌套的跨度、日志或事件
try (Scope unused = span.makeCurrent()) {
  // 注入上下文
  propagator.inject(Context.current(), transportLayer, setter);
  send();
} catch (Exception e) {
  span.recordException(e);
  span.setStatus(StatusCode.ERROR);
  throw e;
} finally {
  span.end();
}

请参阅你所使用语言的OpenTelemetry文档,了解完整的 Java中注入上下文 示例。

也可能有一些例外情况:

  • 下游服务不支持元数据或禁止未知字段
  • 下游服务未定义关联协议。未来的服务版本是否可能支持兼容的上下文传播?注入它吧!
  • 下游服务支持自定义关联协议。
    • 使用自定义传播器进行最佳努力:如果兼容,使用OpenTelemetry跟踪上下文。
    • 或者生成并在跨度上标记自定义相关ID,以适应自定义关联协议。

进程内

  • 使你的跨度活动(也称为当前):它使日志与所有嵌套自动仪表化项的跨度相互关联。

  • 如果库有上下文的概念,请支持可选的显式跟踪上下文传播_以及_活动跨度

    • 将仪表化库创建的跨度(跟踪上下文)明确地放入上下文中,并记录如何访问它
    • 允许用户在你的上下文中传递跟踪上下文
  • 在库内部,显式传播跟踪上下文-活动跨度在回调期间可能会发生变化!

    • 尽早从用户的公共API表面上捕获活动上下文,将其用作跨度的父上下文
    • 围绕传递上下文并在明确传播的实例上完成属性、异常和事件的标记
    • 如果你在某些情况下启动线程、进行后台处理或其他可能会因你语言中的异步上下文流限制而出错的操作,请注意这一点

杂项

仪表化注册表

请将你的仪表化库添加到 OpenTelemetry注册表中,以便用户可以找到它们。

性能

当应用程序没有SDK架构时,OpenTelemetry API是无操作且非常高效的。

真实的应用程序,特别是在高规模的情况下,通常会频繁地配置基于头部的采样方式。 采样输出的跨度是便宜的,你可以检查跨度是否正在记录以避免额外的分配和潜在的昂贵计算,同时填充属性。

// 对于表示采样非常重要的一些属性,应在创建时提供
Span span = tracer.spanBuilder(String.format("SELECT %s.%s", dbName, collectionName))
        .setSpanKind(SpanKind.CLIENT)
        .setAttribute("db.name", dbName)
        ...
        .startSpan();

// 对于那些计算耗时的其他属性,仅在跨度正在记录时添加它们
if (span.isRecording()) {
    span.setAttribute("db.statement", sanitize(query.statement()))
}

错误处理

OpenTelemetry API在运行时是宽容的 【在运行时进行错误处理】, 不会因为无效的参数而失败,从不抛出异常,并会吞掉异常。这样,仪表化问题不会影响应用程序逻辑。测试仪表化以发现OpenTelemetry在运行时隐藏的问题。

测试

由于OpenTelemetry具有各种自动仪表化功能,因此在测试仪表化时,尝试一下你的仪表化与其他遥测的交互:传入请求、传出请求、日志等。在尝试仪表化时,请使用具有流行框架和库以及启用所有跟踪的典型应用程序来检查你的仪表化方式。检查与你的库类似的库如何显示。

对于单元测试,通常可以模拟或伪造SpanProcessorSpanExporter

@Test
public void checkInstrumentation() {
  SpanExporter exporter = new TestExporter();

  Tracer tracer = OpenTelemetrySdk.builder()
           .setTracerProvider(SdkTracerProvider.builder()
              .addSpanProcessor(SimpleSpanProcessor.create(exporter)).build()).build()
           .getTracer("test");
  // ...
  validateSpans(exporter.exportedSpans);
}

class TestExporter implements SpanExporter {
  public final List<SpanData> exportedSpans = Collections.synchronizedList(new ArrayList<>());

  @Override
  public CompletableResultCode export(Collection<SpanData> spans) {
    exportedSpans.addAll(spans);
    return CompletableResultCode.ofSuccess();
  }
  ...
}
最后修改 December 13, 2023: improve glossary translation (46f8201b)