库
OpenTelemetry 为许多库提供了 instrumentation 库,这通常通过库钩子或 monkey-patching 库代码来实现。
使用 OpenTelemetry 进行原生库 instrument 可为用户提供更好的可观察性和开发体验,从而无需库公开和记录钩子。原生 instrument 提供的其他优势包括:
- 自定义日志钩子可以被通用且易于使用的 OpenTelemetry API 替换,用户将只与 OpenTelemetry 交互。
- 来自库和应用程序代码的追踪、日志、指标是相关联且一致的。
- 通用约定允许用户在相同技术以及跨库和语言中获得相似且一致的遥测数据。
- 可以使用广泛的、文档齐全的 OpenTelemetry 可扩展点,对遥测信号进行精细调整(过滤、处理、聚合),以适应各种消费场景。
语义约定
语义约定是有关 Web 框架、RPC 客户端、数据库、消息队列客户端、基础设施等产生的 span 中包含哪些信息的权威来源。约定使 instrument 保持一致:处理遥测数据的用户不必学习库的特定知识,可观察性供应商可以为广泛的技术(例如数据库或消息系统)构建体验。当库遵循约定后,许多场景可以在用户不干预或不进行配置的情况下启用。
语义约定不断发展,新约定也在不断添加。如果您的库还没有相应的约定,请考虑 添加它们。请特别注意 span 名称:努力使用有意义的名称,并在定义时考虑基数。同时设置 schema_url 属性,您可以使用它来记录您正在使用的语义约定版本。
如果您有任何反馈或想添加新的约定,请通过加入 Instrumentation Slack 或在 Specification 仓库 中打开 issue 或 pull request 来贡献。
定义 span
从库用户的角度来思考您的库,以及用户可能想了解的关于库行为和活动的信息。作为库的维护者,您了解其内部工作原理,但用户很可能对库的内部运作不感兴趣,而更关心其应用程序的功能。思考哪些信息有助于分析您的库的使用情况,然后考虑一种适当的方式来建模这些数据。一些需要考虑的方面包括:
- Span 和 span 层级结构
- Span 上的数值属性,作为聚合指标的替代方案
- Span 事件
- 聚合指标
例如,如果您的库正在向数据库发出请求,则仅为对数据库的逻辑请求创建 span。网络上的物理请求应在实现该功能的库中进行 instrument。您还应该倾向于将其他活动(如对象/数据序列化)捕获为 span 事件,而不是作为额外的 span。
设置 span 属性时,请遵循语义约定。
何时不进行 instrument
一些库是包装网络调用的薄客户端。很有可能 OpenTelemetry 已经为底层 RPC 客户端提供了 instrumentation 库。查看 注册表 以查找现有库。如果存在库,则 instrument 包装库可能不是必需的。
作为一般指导,请仅在库的自身层面进行 instrument。如果以下所有情况都适用,则不要进行 instrument:
- 您的库是一个基于文档齐全或不言自明的 API 的薄代理。
- OpenTelemetry 包含底层网络调用的 instrument。
- 没有需要您的库遵循以丰富遥测数据的约定。
如有疑问,请不要进行 instrument。如果您选择不进行 instrument,但仍然可以提供一种方式来配置您内部 RPC 客户端实例的 OpenTelemetry 处理程序。这在不支持完全自动 instrument 的语言中至关重要,在其他语言中也很有用。
本文档的其余部分将指导您了解如何以及对应用程序进行 instrument。
OpenTelemetry API
instrument 应用程序的第一步是将 OpenTelemetry API 包作为依赖项包含进来。
OpenTelemetry 包含 两个主要模块:API 和 SDK。OpenTelemetry API 是一组抽象和非操作实现。除非您的应用程序导入了 OpenTelemetry SDK,否则您的 instrument 将不起任何作用,也不会影响应用程序的性能。
库应仅使用 OpenTelemetry API
如果您担心添加新的依赖项,以下是一些考虑因素,可以帮助您决定如何最大程度地减少依赖冲突:
OpenTelemetry Trace API 于 2021 年初达到稳定状态。它遵循 语义版本控制 2.0。
使用最早的稳定 OpenTelemetry API (1.0.*),除非您必须使用新功能,否则避免更新它。
在您的 instrument 稳定下来时,考虑将其作为一个单独的包发布,这样它就不会给不使用它的用户带来问题。您可以将其保留在您的仓库中,或者 将其添加到 OpenTelemetry,这样它就可以与其他 instrumentation 库一起发布。
语义约定是 稳定的,但可能演变:虽然这不会引起任何功能性问题,但您可能需要偶尔更新您的 instrument。将其放在预览插件或 OpenTelemetry contrib 仓库中,可以帮助保持约定是最新的,而不会对您的用户造成破坏性更改。
获取 tracer
所有应用程序配置都通过 Tracer API 对您的库隐藏。库可能允许应用程序传递 TracerProvider 的实例以方便依赖注入和简化测试,或者从 全局 TracerProvider 获取。OpenTelemetry 语言实现可能会根据每种编程语言的习惯有不同的传递实例或访问全局实例的偏好。
获取 tracer 时,请提供您的库(或 tracing 插件)的名称和版本:它们会显示在遥测数据中,并帮助用户处理和过滤遥测数据,了解其来源,以及调试或报告 instrument 问题。
Instrument 什么
公共 API
公共 API 是 tracing 的良好候选:为公共 API 调用创建的 span 允许用户将遥测数据映射到应用程序代码,了解库调用的持续时间和结果。要 tracing 的调用包括:
- 内部进行网络调用的公共方法,或耗时较长且可能失败的本地操作,例如 I/O。
- 处理请求或消息的 handler。
Instrument 示例
以下示例展示了如何 instrument Java 应用程序:
private static Tracer tracer = getTracer(TracerProvider.noop());
public static void setTracerProvider(TracerProvider tracerProvider) {
tracer = getTracer(tracerProvider);
}
private static Tracer getTracer(TracerProvider tracerProvider) {
return tracerProvider.getTracer("demo-db-client", "0.1.0-beta1");
}
private Response selectWithTracing(Query query) {
// check out conventions for guidance on span names and attributes
Span span = tracer.spanBuilder(String.format("SELECT %s.%s", dbName, collectionName))
.setSpanKind(SpanKind.CLIENT)
.setAttribute("db.name", dbName)
...
.startSpan();
// makes span active and allows correlating logs and nest spans
try (Scope unused = span.makeCurrent()) {
Response response = query.runWithRetries();
if (response.isSuccessful()) {
span.setStatus(StatusCode.OK);
}
if (span.isRecording()) {
// populate response attributes for response codes and other information
}
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getClass().getSimpleName());
throw e;
} finally {
span.end();
}
}
遵循约定填充属性。如果没有适用的约定,请参阅 通用约定。
嵌套的网络和其他 span
网络调用通常通过相应的客户端实现使用 OpenTelemetry 自动 instrument 进行 tracing。
如果 OpenTelemetry 不支持 tracing 您的网络客户端,以下是一些考虑因素,可以帮助您决定最佳行动方案:
- Tracing 网络调用是否能提高用户或您支持他们的可观察性?
- 您的库是否是一个包装了公共、已文档化的 RPC API?如果出现问题,用户是否需要获得底层服务的支持?
- Instrument 该库,并确保 tracing 单个网络尝试。
- 使用 span 追踪这些调用是否会非常冗长?或者是否会显著影响性能?
- 使用具有详细级别的日志或 span 事件:日志可以与父级(公共 API 调用)相关联,而 span 事件应设置在公共 API span 上。
- 如果必须是 span(以携带和传播唯一的 trace 上下文),请将其置于配置选项之后,并默认禁用它们。
如果 OpenTelemetry 已经支持 tracing 您的网络调用,您可能不想重复。可能有一些例外情况:
- 为了支持没有自动 instrument 的用户,在某些环境中可能无法正常工作,或者用户对 monkey-patching 有顾虑。
- 启用自定义或传统的相关性和上下文传播协议与底层服务。
- 使用自动 instrument 未涵盖的必需的库或服务特定信息来丰富 RPC span。
一个避免重复的通用解决方案正在构建中。
事件
Trace 是一种您的应用程序可以发出的信号。事件(或日志)和 Trace 是互补的,而不是重复的。当您有任何需要一定详细程度的内容时,日志比 Trace 是更好的选择。
如果您的应用程序使用了日志或类似的模块,该日志模块可能已经集成了 OpenTelemetry。要了解情况,请参阅 注册表。集成通常会在所有日志中标记活动的 trace 上下文,以便用户可以对其进行关联。
如果您的语言和生态系统没有通用的日志支持,请使用 span 事件 来共享额外的应用程序详细信息。如果您想添加属性,事件可能会更方便。
作为经验法则,使用事件或日志来记录详细数据,而不是 span。始终将事件附加到您的 instrument 创建的 span 实例上。避免使用活动 span,因为您无法控制它指向哪个。
上下文传播
提取上下文
如果您从事接收上游调用的库或服务(例如 Web 框架或消息传递消费者),请从传入的请求或消息中提取上下文。OpenTelemetry 提供了 Propagator API,它隐藏了特定的传播标准并从网络读取 trace Context。对于单个响应,网络上只有一个上下文,它将成为库创建的新 span 的父级。
创建 span 后,通过使 span 处于活动状态,将新的 trace 上下文传递给应用程序代码(回调或 handler);如果可能,请显式执行此操作。以下 Java 示例演示了如何添加 trace 上下文并激活 span。有关更多示例,请参阅 Java 中的 上下文提取。
// extract the context
Context extractedContext = propagator.extract(Context.current(), httpExchange, getter);
Span span = tracer.spanBuilder("receive")
.setSpanKind(SpanKind.SERVER)
.setParent(extractedContext)
.startSpan();
// make span active so any nested telemetry is correlated
try (Scope unused = span.makeCurrent()) {
userCode();
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR);
throw e;
} finally {
span.end();
}
在消息传递系统的情况下,您可能一次收到多条消息。收到的消息将成为您创建的 span 上的链接。有关详细信息,请参阅 消息约定。
注入上下文
当您进行出站调用时,通常希望将上下文传播到下游服务。在这种情况下,创建一个新的 span 来 tracing 出站调用,并使用 Propagator API 将上下文注入消息。可能还有其他情况您可能希望注入上下文,例如在创建用于异步处理的消息时。以下 Java 示例演示了如何传播上下文。有关更多示例,请参阅 Java 中的 上下文注入。
Span span = tracer.spanBuilder("send")
.setSpanKind(SpanKind.CLIENT)
.startSpan();
// make span active so any nested telemetry is correlated
// even network calls might have nested layers of spans, logs or events
try (Scope unused = span.makeCurrent()) {
// inject the context
propagator.inject(Context.current(), transportLayer, setter);
send();
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR);
throw e;
} finally {
span.end();
}
在某些情况下,您可能不需要传播上下文:
- 下游服务不支持元数据或禁止未知字段。
- 下游服务未定义相关协议。请考虑在未来版本中添加对上下文传播的支持。
- 下游服务支持自定义相关协议。
- 尽力使用自定义 propagators:如果兼容,使用 OpenTelemetry trace 上下文,或生成自定义相关 ID 并将其标记在 span 上。
进程内
- 使您的 span 处于活动状态或当前状态,这有助于将 span 与日志和任何嵌套的自动 instrument 相关联。
- 如果库具有上下文概念,请在活动 span 之外支持可选的显式 trace 上下文传播。
- 将库创建的 span(trace 上下文)显式放入上下文,并记录如何访问它。
- 允许用户在您的上下文中传递 trace 上下文。
- 在库内部,显式传播 trace 上下文。活动 span 在回调期间可能会发生变化。
- 尽早从公共 API 表面捕获来自用户的活动上下文,并将其用作 span 的父上下文。
- 传递上下文并在显式传播的实例上标记属性、异常、事件。
- 如果启动线程、执行后台处理或其他可能因语言的异步上下文流限制而出现问题的操作,这一点至关重要。
其他注意事项
Instrument 注册表
将您的 instrumentation 库添加到 OpenTelemetry 注册表,以便用户可以找到它。
性能
当应用程序中没有 SDK 时,OpenTelemetry API 是 no-op 且性能非常高。当配置了 OpenTelemetry SDK 时,它会 消耗绑定的资源。
真实的应用程序,尤其是在大规模应用中,通常会配置基于头部的采样。被采样掉的 span 是可承受的,您可以检查 span 是否正在记录,以避免额外的分配和可能昂贵的计算,同时填充属性。以下 Java 示例展示了如何为采样提供属性并检查 span 记录。
// some attributes are important for sampling, they should be provided at creation time
Span span = tracer.spanBuilder(String.format("SELECT %s.%s", dbName, collectionName))
.setSpanKind(SpanKind.CLIENT)
.setAttribute("db.name", dbName)
...
.startSpan();
// other attributes, especially those that are expensive to calculate
// should be added if span is recording
if (span.isRecording()) {
span.setAttribute("db.statement", sanitize(query.statement()))
}
错误处理
OpenTelemetry API 在无效参数时不会失败,从不抛出异常,并且会吞噬异常,这意味着它 运行时容忍度高。这样,instrument 问题就不会影响应用程序逻辑。测试 instrument 以注意 OpenTelemetry 在运行时隐藏的问题。
测试
由于 OpenTelemetry 有各种自动 instrument,请尝试让您的 instrument 与其他遥测数据进行交互:传入请求、出站请求、日志等。使用典型的应用程序,包含流行的框架和库,并在尝试您的 instrument 时启用所有 tracing。查看类似您库的库是如何显示的。
对于单元测试,您通常可以像下面的 Java 示例一样,模拟或伪造 SpanProcessor 和 SpanExporter。
@Test
public void checkInstrumentation() {
SpanExporter exporter = new TestExporter();
Tracer tracer = OpenTelemetrySdk.builder()
.setTracerProvider(SdkTracerProvider.builder()
.addSpanProcessor(SimpleSpanProcessor.create(exporter)).build()).build()
.getTracer("test");
// run 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();
}
...
}