OpenTelemetry Node.js Tracing 问题排查清单
博客文章在发布后不会更新。这篇文章已经发布一年多了,其内容可能已过时,部分链接可能无效。在依赖任何信息之前,请务必核实。
我会尽量让这篇文章简短而切中要点。你可能在这里是因为你已经在 Node.js 应用程序中安装了 OpenTelemetry,但没有看到任何 traces,或者一些预期的 spans 丢失了。
原因可能有很多,但有些比其他原因更常见。在这篇文章中,我将尝试列出常见的原因,以及一些诊断方法和技巧。
要求
我假设你已经具备了 OpenTelemetry 是什么以及它是如何工作的基本知识,并且你已经在你的 Node.js 应用程序中尝试过设置它。
启用日志记录
OpenTelemetry JS 默认不会向其诊断日志记录器记录任何内容。下面 SDK 的大多数问题在启用日志记录器后都可以轻松检测到。
你可以在服务中的尽可能早的地方添加以下代码,将所有内容记录到控制台:
// tracing.ts or main index.ts
import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api';
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
// rest of your otel initialization code
这对于调试很有用。在生产环境中将所有内容记录到控制台不是一个好主意,因此请记住在问题解决后删除或禁用它。
专业提示:你可以使用 OTEL_LOG_LEVEL 环境变量来设置 DiagLogLevel,这样我们就可以轻松地打开和关闭它。
自动插装库
许多用户选择使用自动插装库,这些库可以自动为流行和广泛使用的软件包(数据库驱动程序、HTTP 框架、云服务 SDK 等)中的重要操作创建 spans。
一些初始化模式和配置选项可能导致你的服务根本无法创建 spans。
要排除自动插装库的问题,请先尝试创建手动 span。如果你看到手动 spans 但未看到已安装的自动插装库生成的 spans,请继续阅读本节。
import { trace } from '@opentelemetry/api';
trace
.getTracerProvider()
.getTracer('debug')
.startSpan('test manual span')
.end();
安装和启用
要在你的服务中使用自动插装库,你需要
- 安装它:
npm install @opentelemetry/instrumentation-foo。你可以在 OpenTelemetry Registry 中搜索可用的插装项。 - 创建插装对象:
new FooInstrumentation(config) - 确保插装已启用:调用
registerInstrumentations(...) - 验证你使用的是正确的 TracerProvider
对于大多数用户,以下代码应该可以满足需求:
// First run: npm install @opentelemetry/instrumentation-foo @opentelemetry/instrumentation-bar
// Replace foo and bar with the actual packages you need to instrument (HTTP/mySQL/Redis etc)
import { FooInstrumentation } from '@opentelemetry/instrumentation-foo';
import { BarInstrumentation } from '@opentelemetry/instrumentation-bar';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
// create TracerProvider, SpanProcessors and SpanExporters
registerInstrumentations({
instrumentations: [new FooInstrumentation(), new BarInstrumentation()],
});
对于选择使用低级 API 而不是调用 registerInstrumentations 的高级用户,请确保你的插装已设置为使用正确的 tracer provider,并且在适当时调用 enable()。
在 `require` 之前启用
所有插装项的设计都是这样的:你必须先启用它们,然后再 `require` 被插装的包。一个常见的错误是在为包启用插装库之前 `require` 这些包。
这是一个**错误**的示例:
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import {
SimpleSpanProcessor,
ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-base';
import http from 'http'; // ⇐ BAD - at this point instrumentation is not registered yet
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
provider.register();
registerInstrumentations({ instrumentations: [new HttpInstrumentation()] });
// your application code which uses http
在大多数情况下,插装代码驻留在与应用程序代码不同的文件或包中,这使得发现它很棘手。某些框架(如 serverless)可能会在插装代码有机会运行时之前就导入包。这很容易被忽略。
要诊断此问题,请启用日志记录并验证你是否看到你的插装库正在被加载。例如:
@opentelemetry/instrumentation-http Applying patch for https@12.22.9
如果缺失,那么你的自动插装库很可能没有被应用。
库配置
一些自动插装库包含自定义配置,用于控制何时**跳过插装**。例如,HTTP 插装具有 ignoreIncomingRequestHook 和 requireParentforOutgoingSpans 等选项。
在某些特定情况下,一些库**默认不进行插装**,你需要特别选择加入才能获得 spans。例如,`ioredis` 插装应该用 requireParentSpan = true 配置,以便为没有父 span 的内部操作创建 spans。
如果你没有看到某个库的 spans,也许你需要调整配置使它们出现。
被插装库的版本
自动插装库通常不支持被插装库的所有版本。如果你使用的版本太旧或太新,它可能不受支持,因此不会创建 spans。
查阅你所使用的库的文档,以验证你的版本是否兼容。这些数据通常可以在插装项的 README 中找到,例如查看 Redis README。
未记录和未采样的 Span
并非你应用程序中创建的所有 spans 都会被导出。Span 可以被标记为“未采样”或“未记录”,在这种情况下你不会在后端看到它们。
要排除这些问题,你可以挂载一个“debug span processor”,它只打印采样决策。如果控制台打印出“span sampled: false”,则继续阅读本节。
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
import { trace, Span, Context, TraceFlags } from '@opentelemetry/api';
const provider = new NodeTracerProvider();
provider.addSpanProcessor({
forceFlush: async () => {},
onStart: (_span: Span, _parentContext: Context) => {},
onEnd: (span: ReadableSpan) => {
const sampled = !!(span.spanContext().traceFlags & TraceFlags.SAMPLED);
console.log(`span sampled: ${sampled}`);
},
shutdown: async () => {},
});
provider.register();
NoopTracerProvider
如果你没有创建和注册一个有效的 TracerProvider,你的应用程序将运行默认的 TracerProvider,它会将你应用程序中的所有 spans 都视为 NonRecordingSpans。
你需要在应用程序的尽可能早的地方包含类似以下的代码:
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import {
ConsoleSpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-base';
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
provider.register();
远程采样决策
默认的采样行为(也是非常流行的一种)是每个 span 继承其父节点的采样决策。如果调用你服务的组件**配置为不采样**,那么你**也看不到**你服务的 spans。
例如
- API Gateway 可以配置采样逻辑或禁用 tracing,在这种情况下,它会影响所有下游 tracing(包括你无辜的服务,而你需要对其进行采样)。
- 调用你服务的外部用户也可以被插装并得出自己的采样决策(而你对此无能为力)。然后,这些采样决策会被传播到你的服务并影响它。
- 你系统中的其他服务可以根据其本地需求和视角得出采样决策。可以很容易地将一个上游服务端点配置为不对一个不重要的端点进行采样,而没有意识到它调用了一个下游非常重要且需要进行采样的端点。
本地采样器
你可以配置本地采样器来采样一些 spans 或不采样任何 spans。如果配置是由其他人很久以前编写的,或者配置很复杂/不直观——那么 spans 被不采样和导出就情有可原,这很容易被忽略。
导出问题
有可能你的服务正在生成 spans,但它们没有被正确导出到你的后端,或者因为某些原因被 collector 拒绝了。
要排除导出问题,请尝试添加“ConsoleExporter”。如果你在控制台看到 spans 被导出,但在你导出的后端看不到,请继续阅读本节。
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import {
ConsoleSpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-base';
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
provider.register();
配置 Exporter
你的服务应该有类似以下的 span 导出代码:
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
// Create TracerProvider
const exporter = new OTLPTraceExporter();
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
在这个例子中,我使用了 `@opentelemetry/exporter-trace-otlp-proto`,但还有其他 Exporter 可供选择,每个 Exporter 都有一些配置选项。其中一个选项的错误会导致导出失败,而这默认会被静默忽略。
以下子节涵盖了一些常见的配置错误。
OTLP Exporters
- 格式 — OTLP 支持 `http/json`、`http/proto` 和 `grpc` 格式。你需要选择一个与你的 OTLP collector 支持的格式匹配的 Exporter 包。
- 路径 — 如果你设置了 HTTP collector 端点(通过代码中的配置或环境变量),**你必须同时设置路径**:`http://my-collector-host:4318/v1/traces`。如果你忘记了路径,导出将失败。在 gRPC 中,你一定不要添加路径:`“grpc://:4317”`。这在一开始可能有点令人困惑。
- 安全连接 — 检查你的 collector 是否需要安全连接或不安全连接。在 HTTP 中,这由 URL 方案(`http:` / `https:`)决定。在 gRPC 中,方案没有影响,连接安全性完全由 credentials 参数设置:`grpc.credentials.createSsl()`、`grpc.credentials.createInsecure()` 等。HTTP 和 gRPC 的默认安全性是**不安全**的。
Jaeger Exporter
Jaeger Exporter 可以工作在“Agent”模式(通过 UDP)和“Collector”模式(通过 TCP)。决定使用哪种模式的逻辑有点令人困惑且文档缺乏。如果你在 exporter 配置中传递了 `endpoint` 参数或设置了 `OTEL_EXPORTER_JAEGER_ENDPOINT` 环境变量,那么 exporter 将使用“Collector” HTTP 发送器。否则,它将以“Agent”模式通过 UDP 发送器导出到 `param` 中配置的 `host`,或者 `OTEL_EXPORTER_JAEGER_AGENT_HOST` 或 `localhost:6832`。
设置供应商凭证
如果你使用供应商作为你的 tracing 后端,你可能需要添加额外信息,如认证头。例如,如果你将 traces 发送到 Aspecto,你需要像这样添加你的 Aspecto token 作为 Authorization 头:
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
// Create TracerProvider
const exporter = new OTLPTraceExporter({
url: 'https://otelcol.aspecto.io/v1/trace',
headers: {
Authorization: 'YOUR_API_KEY_HERE',
},
});
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
如果未应用,你将无法在你的供应商账户中看到任何数据。
Flush 和 Shutdown
当你的服务关闭或你的 lambda 函数结束时,可能并非所有 spans 都已成功导出到你的 collector。你需要调用 tracer provider 上的 shutdown 函数并 await 返回的 Promise,以确保所有数据都已发送。
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
const provider = new NodeTracerProvider();
provider.register();
// when your you terminate your service, call shutdown on provider:
provider.shutdown();
包版本兼容性
一些问题可能是由于 SDK 和插装库版本不兼容或过旧造成的。
SDK 版本
建议检查你的 SDK 和 API 包是否过时以及它们之间是否兼容。确保你在 `npm install` 时没有收到任何 peer 依赖警告。
其他 APM 库
OpenTelemetry 不能保证与使用 monkey patching 来实现其功能的其他 APM 库兼容。如果你安装了此类包,请尝试删除或禁用它,并检查问题是否消失。
下一步?
在哪里获得帮助
如果以上方法都未能解决你的问题,你可以在以下渠道寻求帮助:
- CNCF
#otel-jsSlack 频道 - CNCF
#opentelemetry-bootcampSlack 频道 - GitHub 讨论区
资源
我应该使用供应商吗?
另一个选择是使用供应商提供的 OpenTelemetry 发行版。这些发行版可以为你节省时间和精力:
- 技术支持
- 预先配置了针对普通用户和高级用户的流行功能
- 与最新的 OpenTelemetry 版本保持同步
- 实施最佳实践并避免上述陷阱
有关 OpenTelemetry 供应商列表,请参阅 供应商。
本文的一个版本最初发布在 Aspecto 博客上。