--- title: OpenTelemetry description: "学习如何在 Deno 应用程序中使用 OpenTelemetry 实现可观察性。涵盖追踪、指标收集和与监控系统的集成。" --- Deno 内置支持 [OpenTelemetry](https://opentelemetry.io/)。 > OpenTelemetry 是一组 API、SDK 和工具。使用它来对软件进行仪器化,生成、收集和导出遥测数据(指标、日志和追踪)以帮助您分析软件的性能和行为。 > > - https://opentelemetry.io/ 此集成让您能够使用 OpenTelemetry 的可观察性工具(如日志、指标和追踪)来监控您的 Deno 应用程序。 Deno 提供以下功能: - 使用 OpenTelemetry 协议,将收集的指标、追踪和日志导出到服务器。 - 对 Deno 运行时进行[自动仪器化](#自动仪器化),采集 OpenTelemetry 的指标、追踪和日志。 - [收集使用 `npm:@opentelemetry/api` 包创建的用户自定义指标、追踪和日志](#用户指标)。 ## 快速开始 要启用 OpenTelemetry 集成,请设置环境变量 `OTEL_DENO=true`: ```sh OTEL_DENO=true deno run my_script.ts ``` 这将自动收集并通过 HTTP 的 Protobuf 格式(`http/protobuf`)将运行时可观察性数据导出到 `localhost:4318` 的 OpenTelemetry 端点。 :::tip 如果您还未搭建 OpenTelemetry 收集器,可以使用以下命令快速启动 [Docker 中的本地 LGTM 堆栈](https://github.com/grafana/docker-otel-lgtm/tree/main?tab=readme-ov-file)(Loki(日志)、Grafana(仪表盘)、Tempo(追踪)和 Prometheus(指标)): ```sh docker run --name lgtm -p 3000:3000 -p 4317:4317 -p 4318:4318 --rm -ti \ -v "$PWD"/lgtm/grafana:/data/grafana \ -v "$PWD"/lgtm/prometheus:/data/prometheus \ -v "$PWD"/lgtm/loki:/data/loki \ -e GF_PATHS_DATA=/data/grafana \ docker.io/grafana/otel-lgtm:0.8.1 ``` 然后,您可以使用用户名 `admin` 和密码 `admin` 登录 Grafana 仪表盘,地址为 `http://localhost:3000`。 ::: 这将自动收集并导出运行时的可观察性数据,例如 `console.log`、HTTP 请求的追踪和 Deno 运行时指标。 [了解更多关于自动仪器化的信息](#自动仪器化)。 您也可以使用 `npm:@opentelemetry/api` 包创建自己的指标、追踪和日志。 [了解更多关于用户定义的指标](#用户指标)。 ## 自动仪器化 Deno 会自动收集并将部分可观察性数据导出到 OTLP 端点。 这些数据通过名为 `deno` 的内置仪器化范围导出,该范围名称为 `deno`,版本即 Deno 运行时的版本。例如,`deno:2.1.4`。 ### 追踪 Deno 会自动为多种操作创建跨度,例如: - 使用 `Deno.serve` 提供的传入 HTTP 请求。 - 使用 `fetch` 发出的传出 HTTP 请求。 #### `Deno.serve` 当您使用 `Deno.serve` 创建 HTTP 服务器时,系统会为每个传入请求创建一个跨度。该跨度在响应头发送时自动结束(而非等待响应体完全发送)。 创建的跨度名称为 `${method}`,跨度类型为 `server`。 创建跨度时自动添加如下属性: - `http.request.method`:请求的 HTTP 方法。 - `url.full`:请求的完整 URL(等同于 `req.url`)。 - `url.scheme`:请求 URL 的协议(例如 `http` 或 `https`)。 - `url.path`:请求 URL 的路径部分。 - `url.query`:请求 URL 的查询字符串。 请求处理完成后,会添加下列属性: - `http.response.status_code`:响应的状态码。 Deno 不会自动向跨度添加 `http.route` 属性,因为运行时无法知道路由细节,路由由用户处理函数中的逻辑决定。如需添加 `http.route`,请在处理函数中使用 `npm:@opentelemetry/api` 设置该属性,同时建议更新跨度名称以包含路由信息。 ```ts import { trace } from "npm:@opentelemetry/api@1"; const INDEX_ROUTE = new URLPattern({ pathname: "/" }); const BOOK_ROUTE = new URLPattern({ pathname: "/book/:id" }); Deno.serve(async (req) => { const span = trace.getActiveSpan(); if (INDEX_ROUTE.test(req.url)) { span.setAttribute("http.route", "/"); span.updateName(`${req.method} /`); // 处理首页路由 } else if (BOOK_ROUTE.test(req.url)) { span.setAttribute("http.route", "/book/:id"); span.updateName(`${req.method} /book/:id`); // 处理图书路由 } else { return new Response("未找到", { status: 404 }); } }); ``` #### `fetch` 当您使用 `fetch` 发出请求时,系统会为该请求创建一个跨度,且在收到响应头时自动结束。 创建的跨度名称为 `${method}`,跨度类型为 `client`。 创建跨度时自动添加如下属性: - `http.request.method`:请求的 HTTP 方法。 - `url.full`:请求的完整 URL。 - `url.scheme`:请求 URL 的协议。 - `url.path`:请求 URL 的路径部分。 - `url.query`:请求 URL 的查询字符串。 收到响应后,添加以下属性: - `http.status_code`:响应的状态码。 ### 指标 自动收集和导出的指标包括: #### `Deno.serve` / `Deno.serveHttp` ##### `http.server.request.duration` 一个直方图,记录通过 `Deno.serve` 或 `Deno.serveHttp` 提供的传入 HTTP 请求的持续时间。测量时间为从请求接收至响应头发送的时间,不包含发送响应体的时间。单位为秒。该直方图的桶边界为: `[0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0]`。 该指标记录以下属性: - `http.request.method`:请求的 HTTP 方法。 - `url.scheme`:请求 URL 的协议。 - `network.protocol.version`:请求所使用的 HTTP 协议版本(例如 `1.1` 或 `2`)。 - `server.address`:服务器监听的地址。 - `server.port`:服务器监听的端口。 - `http.response.status_code`:响应状态码(请求无致命错误时)。 - `error.type`:发生的错误类型(请求处理出现错误时)。 ##### `http.server.active_requests` 一个量表,统计任意时刻由 `Deno.serve` 或 `Deno.serveHttp` 正在处理的活动请求数量,即已接收但尚未发送响应头的请求数。该指标记录以下属性: - `http.request.method`:请求的 HTTP 方法。 - `url.scheme`:请求 URL 的协议。 - `server.address`:服务器监听的地址。 - `server.port`:服务器监听的端口。 ##### `http.server.request.body.size` 一个直方图,记录传入 HTTP 请求的请求体大小,单位为字节。直方图桶边界为: `[0, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000]`。 记录属性: - `http.request.method`:请求的 HTTP 方法。 - `url.scheme`:请求 URL 的协议。 - `network.protocol.version`:请求所使用的 HTTP 协议版本。 - `server.address`:服务器监听的地址。 - `server.port`:服务器监听的端口。 - `http.response.status_code`:响应状态码(无致命错误时)。 - `error.type`:错误类型(处理异常时)。 ##### `http.server.response.body.size` 一个直方图,记录传入 HTTP 请求的响应体大小,单位为字节。直方图桶边界同请求体大小指标。 记录属性: - `http.request.method`:请求的 HTTP 方法。 - `url.scheme`:请求 URL 的协议。 - `network.protocol.version`:请求所使用的 HTTP 协议版本。 - `server.address`:服务器监听的地址。 - `server.port`:服务器监听的端口。 - `http.response.status_code`:响应状态码(无致命错误时)。 - `error.type`:错误类型(处理异常时)。 ### 日志 自动收集并导出的日志包括: - 使用 `console.*` 方法(如 `console.log`、`console.error`)产生的任意日志。 - 由 Deno 运行时产生的日志,如调试日志、下载日志等。 - 导致 Deno 运行时退出的任何错误(无论来自用户代码还是运行时本身)。 在 JavaScript 代码中发生的日志会携带相关的跨度上下文(如果日志发生在活跃跨度内)。 `console` 自动仪器化可以通过环境变量 `OTEL_DENO_CONSOLE` 配置: - `capture`:日志既会输出到 stdout/stderr,也会被导出到 OpenTelemetry。(默认) - `replace`:日志不输出到 stdout/stderr,只有导出到 OpenTelemetry。 - `ignore`:日志只输出到 stdout/stderr,不导出到 OpenTelemetry。 ## 用户指标 除自动收集的遥测数据外,您还可以使用 `npm:@opentelemetry/api` 包创建自己的指标和追踪。 您无需特别配置 `npm:@opentelemetry/api` 包以便在 Deno 中使用,Deno 会自动完成配置。无需调用 `metrics.setGlobalMeterProvider()`、 `trace.setGlobalTracerProvider()` 或 `context.setGlobalContextManager()`。所有资源配置、导出设置等均通过环境变量实现。 Deno 与版本 `1.x` 的 `npm:@opentelemetry/api` 包兼容。您既可以直接从 `npm:@opentelemetry/api@1` 导入,也可以使用 `deno add` 本地安装该包后由 `@opentelemetry/api` 导入。 ```sh deno add npm:@opentelemetry/api@1 ``` 在追踪和指标中,您需要为跟踪器和仪表分别命名。如果您为库做仪器化,应使用库名(如 `my-awesome-lib`);如果为应用程序做仪器化,应使用应用名(如 `my-app`)。版本应设置为库或应用的版本。 ### 追踪 要创建新跨度,先从 `npm:@opentelemetry/api` 导入 `trace` 并获取一个跟踪器: ```ts import { trace } from "npm:@opentelemetry/api@1"; const tracer = trace.getTracer("my-app", "1.0.0"); ``` 之后使用 `tracer.startActiveSpan` 创建一个新的跨度并传入回调。需在回调中手动调用 `span.end()` 结束该跨度。 ```ts function myFunction() { return tracer.startActiveSpan("myFunction", (span) => { try { // 执行 myFunction 的工作 } catch (error) { span.recordException(error); span.setStatus({ code: trace.SpanStatusCode.ERROR, message: (error as Error).message, }); throw error; } finally { span.end(); } }); } ``` 应在 `finally` 中调用 `span.end()` 以保证跨度结束,无论是否发生异常。异常时调用 `span.recordException` 和 `span.setStatus` 以便于记录。 在回调内部,创建的跨度是“活动跨度”,可使用 `trace.getActiveSpan()` 访问。此“活动跨度”在回调及其调用函数内使用时会作为父跨度。 [了解更多关于上下文传播的信息](#上下文传播)。 `startActiveSpan` 返回回调函数的返回值。 在跨度生命周期内,您可以调用 `setAttribute` 和 `setAttributes` 添加属性,即结构化元数据键值对。 ```ts span.setAttribute("key", "value"); span.setAttributes({ success: true, "bar.count": 42n, "foo.duration": 123.45 }); ``` 属性值支持字符串、数字(浮点)、大整数(限制为 u64)、布尔值,或者这些类型的数组。其它类型会被忽略。 可以用 `updateName` 方法更改跨度名称: ```ts span.updateName("new name"); ``` 且可用 `setStatus` 设置跨度状态。`recordException` 用于记录生命周期内的异常,创建带堆栈跟踪和名称的事件附加到跨度。**`recordException` 不会设置状态为 `ERROR`,需手动调用 `setStatus`**。 ```ts import { SpanStatusCode } from "npm:@opentelemetry/api@1"; span.setStatus({ code: SpanStatusCode.ERROR, message: "发生了一个错误", }); span.recordException(new Error("发生了一个错误")); // 或 span.setStatus({ code: SpanStatusCode.OK, }); ``` Spans can also have [events](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api._opentelemetry_api.Span.html#addevent) and [links](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api._opentelemetry_api.Span.html#addlink) added to them. Events are points in time that are associated with the span. Links are references to other spans. ```ts // 向跨度添加事件 span.addEvent("button_clicked", { id: "submit-button", action: "submit", }); // 带时间戳的事件 span.addEvent("process_completed", { status: "success" }, Date.now()); ``` 事件可包含类似于跨度的可选属性。它们用于标记跨度生命周期内重要时刻,无需创建额外跨度。 您也可以用 `tracer.startSpan` 手动创建跨度,该方法返回跨度对象。此方法不会设置创建的跨度为活动跨度,故后续创建的跨度或 `console.log` 不会自动继承它。可配合 [上下文传播 API](#上下文传播) 手动将此跨度设置为活动跨度。 `tracer.startActiveSpan` 和 `tracer.startSpan` 可接受含以下任意属性的选项参数: - `kind`: 跨度类型。可为 `SpanKind.CLIENT`、`SpanKind.SERVER`、`SpanKind.PRODUCER`、`SpanKind.CONSUMER` 或 `SpanKind.INTERNAL`。默认值为 `SpanKind.INTERNAL`。 - `startTime`:表示跨度开始时间的 `Date` 对象,或自 Unix 纪元起的毫秒数。如果未提供,则使用当前时间。 - `attributes`: 要添加到跨度的属性对象。 - `links`: 要添加到跨度的链接数组。 - `root`: 布尔值,表示跨度是否为根跨度。如果为 `true`,则该跨度没有父跨度(即使存在活动跨度)。 在选项参数之后,`tracer.startActiveSpan` 和 `tracer.startSpan` 还可以接收来自 [上下文传播 API](#上下文传播) 的 `context` 对象。 了解完整追踪 API 请参考 [OpenTelemetry JS API 文档](https://open-telemetry.github.io/opentelemetry-js/classes/_opentelemetry_api._opentelemetry_api.TraceAPI.html)。 ### 指标 要创建指标,首先从 `npm:@opentelemetry/api` 导入 `metrics` 对象并创建一个仪表: ```ts import { metrics } from "npm:@opentelemetry/api@1"; const meter = metrics.getMeter("my-app", "1.0.0"); ``` 然后用仪表创建仪器,用于记录数值: ```ts const counter = meter.createCounter("my_counter", { description: "一个简单的计数器", unit: "1", }); counter.add(1); counter.add(2); ``` 记录时也可附带属性: ```ts counter.add(1, { color: "red" }); counter.add(2, { color: "blue" }); ``` :::tip OpenTelemetry 中建议指标属性保持低基数,即属性值的唯一组合数量不宜过多。例如,用户所属洲的属性是低基数,但用户的精确经纬度则属于高基数。高基数属性可能导致指标存储与导出问题,推荐通过跨度和日志处理高基数数据。 ::: 常见仪器类型: - **计数器**:单调递增的数值,只能增加。适合计数处理请求数等。 - **上下计数器**:可增可减,适合表示活动连接数、进行中请求数。 - **仪表**:可以任意设置值,适合非累积值,如当前温度。 - **直方图**:记录数值分布。例如请求响应时间(毫秒),用于计算百分位、平均等统计。预定义桶边界默认为 `[0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 750.0, 1000.0, 2500.0, 5000.0, 7500.0, 10000.0]`。 还有几类可观察(异步)仪器,没有同步记录方法,而是提供回调用于异步上报值,OpenTelemetry SDK 在导出前调用回调。 ```ts const counter = meter.createObservableCounter("my_counter", { description: "一个简单的计数器", unit: "1", }); counter.addCallback((res) => { res.observe(1); // 或 res.observe(1, { color: "red" }); }); ``` 存在三种可观察仪器类型: - **ObservableCounter**:异步可观察的计数器。用于常增值,如处理的请求数。 - **ObservableUpDownCounter**:异步可观察的上下计数器。值能增减,如活动连接数或进行中请求数。 - **ObservableGauge**:异步可观察的仪表。用于任意值,如当前温度。 了解完整指标 API 请参考 [OpenTelemetry JS API 文档](https://open-telemetry.github.io/opentelemetry-js/classes/_opentelemetry_api._opentelemetry_api.MetricsAPI.html)。 ### 实际示例 有关在 Deno 应用中实现 OpenTelemetry 的实际示例,请参见教程: - [基础 OpenTelemetry 教程](/examples/basic_opentelemetry_tutorial/) - 一个带有自定义指标和追踪的简单 HTTP 服务器 - [分布式追踪教程](/examples/otel_span_propagation_tutorial/) - 跨服务边界追踪的高级技巧 ## 上下文传播 在 OpenTelemetry 中,上下文传播是指将一些上下文信息(如当前跨度)从应用的一个部分传递到另一个部分,而无需手动将其作为参数传递给每个函数。 在 Deno 中,上下文传播遵循 TC39 提案的 `AsyncContext` 规则。`AsyncContext` API 尚未向用户公开,但内部用于在异步边界上传播活动跨度和其他上下文信息。 简要说明 AsyncContext 传播工作方式: - 当启动一个新的异步任务(例如 Promise 或定时器)时,当前上下文会被保存。 - 其他代码可以在不同上下文中并发执行。 - 当异步任务完成时,保存的上下文被恢复。 这意味着异步上下文传播类似于一个全局变量,其作用域限定于当前异步任务,并自动拷贝到由当前任务启动的新异步任务中。 来自 `npm:@opentelemetry/api@1` 的 `context` API 向用户暴露此功能。用法如下: ```ts import { context } from "npm:@opentelemetry/api@1"; // 获取当前活动上下文 const currentContext = context.active(); // 创建新的上下文并设值 const newContext = currentContext.setValue("id", 1); // 设值不改变当前上下文 console.log(currentContext.getValue("id")); // undefined // 在新上下文内执行 context.with(newContext, () => { console.log(context.active().getValue("id")); // 1 function myFunction() { return context.active().getValue("id"); } console.log(myFunction()); // 1 setTimeout(() => { console.log(context.active().getValue("id")); // 1 }, 10); }); // 外部上下文未变 console.log(context.active().getValue("id")); // undefined ``` 上下文 API 与跨度集成,您可以将跨度装入上下文并在上下文中执行函数: ```ts import { context, trace } from "npm:@opentelemetry/api@1"; const tracer = trace.getTracer("my-app", "1.0.0"); const span = tracer.startSpan("myFunction"); const contextWithSpan = trace.setSpan(context.active(), span); context.with(contextWithSpan, () => { const activeSpan = trace.getActiveSpan(); console.log(activeSpan === span); // true }); // 记得结束跨度! span.end(); ``` 了解完整上下文 API 请参考 [OpenTelemetry JS API 文档](https://open-telemetry.github.io/opentelemetry-js/classes/_opentelemetry_api._opentelemetry_api.ContextAPI.html)。 ## 配置 通过设置环境变量 `OTEL_DENO=true` 启用 OpenTelemetry 集成。 OTLP 导出端点和协议可通过环境变量 `OTEL_EXPORTER_OTLP_ENDPOINT` 和 `OTEL_EXPORTER_OTLP_PROTOCOL` 配置。 若端点需要身份验证,可使用环境变量 `OTEL_EXPORTER_OTLP_HEADERS` 设置请求头。 指标、追踪和日志导出的端点也可用以下专用变量分别覆盖,例如: - `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` - `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` 有关 OTLP 头部配置详情,请参考 [OpenTelemetry 官网](https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options)。 遥测数据关联的资源属性通过环境变量 `OTEL_SERVICE_NAME` 和 `OTEL_RESOURCE_ATTRIBUTES` 配置。除 `OTEL_RESOURCE_ATTRIBUTES` 设置的属性外,自动添加: - `service.name`:未设置 `OTEL_SERVICE_NAME` 时值为 ``。 - `process.runtime.name`:`deno`。 - `process.runtime.version`:Deno 运行时版本。 - `telemetry.sdk.name`:`deno-opentelemetry`。 - `telemetry.sdk.language`:`deno-rust`。 - `telemetry.sdk.version`:Deno 版本与使用的 `opentelemetry` Rust crate 版本,通过 `-` 连接。 传播器可通过 `OTEL_PROPAGATORS` 环境变量配置,默认值为 `tracecontext,baggage`,用逗号分隔多项,目前支持: - `tracecontext`:W3C Trace Context 格式。 - `baggage`:W3C Baggage 格式。 指标收集频率使用 `OTEL_METRIC_EXPORT_INTERVAL` 配置,默认 60000 毫秒(60 秒)。 跨度导出批处理配置参考 [OpenTelemetry 规范](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#batch-span-processor)。 日志导出批处理配置参考 [OpenTelemetry 规范](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#batch-log-record-processor)。 ## 传播器 Deno 支持上下文传播器,用以自动跨进程边界传播追踪上下文,实现分布式追踪,跟踪请求在服务链中流转。 传播器负责在载体中编码(如 HTTP 头)并解码上下文信息(如追踪与跨度 ID),从而实现服务间追踪上下文传递。 默认支持以下传播器: - `tracecontext`:W3C Trace Context 传播格式,是 HTTP 头传播追踪上下文的标准方式。 - `baggage`:W3C Baggage 格式,允许跨服务传递键值对。 :::note 这些传播器会自动与 Deno 的 `fetch` 和 `Deno.serve` 配合,使 HTTP 请求可以端到端自动传播追踪上下文,无需手动管理。 ::: 您可以通过 `@opentelemetry/api` 包访问传播 API: ```ts import { context, propagation, trace } from "npm:@opentelemetry/api@1"; // 从传入 HTTP 头提取上下文 function extractContextFromHeaders(headers: Headers) { const ctx = context.active(); return propagation.extract(ctx, headers); } // 向传出 HTTP 头注入上下文 function injectContextIntoHeaders(headers: Headers) { const ctx = context.active(); propagation.inject(ctx, headers); return headers; } // 示例:进行带有传播追踪上下文的 fetch 请求 async function tracedFetch(url: string) { const headers = new Headers(); injectContextIntoHeaders(headers); return await fetch(url, { headers }); } ``` ## 限制 Deno 的 OpenTelemetry 集成仍在开发中,存在以下限制: - 追踪始终被采样(即 `OTEL_TRACE_SAMPLER=parentbased_always_on`)。 - 追踪只支持无属性的链接。 - 不支持指标采样。 - 不支持自定义日志流(仅支持 `console.log` 和 `console.error` 日志)。 - 唯一支持的导出器是 OTLP,其他导出器未支持。 - OTLP 仅支持 `http/protobuf` 和 `http/json` 协议,不支持 `grpc` 等。 - 可观察(异步)仪表采集的指标在进程退出或崩溃时不会被采集,故最后指标可能未导出。同步指标可正常导出。 - 追踪跨度不遵守环境变量中指定的 `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT`、`OTEL_ATTRIBUTE_COUNT_LIMIT`、`OTEL_SPAN_EVENT_COUNT_LIMIT`、`OTEL_SPAN_LINK_COUNT_LIMIT`、`OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT` 和 `OTEL_LINK_ATTRIBUTE_COUNT_LIMIT` 限制。 - 不尊重环境变量 `OTEL_METRIC_EXPORT_TIMEOUT`。 - 未知的 HTTP 方法不会在 `http.request.method` 跨度属性中标准化为 `_OTHER`,与 OpenTelemetry 语义约定不符。 - `Deno.serve` 的 HTTP 服务器跨度在处理程序抛出(调用 `onError`)时不会设置错误状态,错误也不通过事件附加到跨度。 - `fetch` 的 HTTP 客户端跨度中无机制添加 `http.route` 属性或更新跨度名称以包含路由信息。