generated at
Deno v2.1で導入されたOpenTelemetryサポートを試してみる
はじめに
Deno Advent Calendar 2024 20日目の記事です🎅🎄
Deno v2.1OpenTelemetryのサポートが実験的に導入されたので、試してみました
⚠️ DenoOpenTelemetryのサポートはまだ安定化はされていないため、今後、破壊的変更が導入される可能性もあります
⚠️ 筆者はOpenTelemetryに詳しくなく、ところどころ一般的な規約やベストプラクティスなどに従えていない箇所があるかもしれません🙏

目的
Deno v2.1で入ったOpenTelemetryサポートに関する紹介
deno fmtYAMLサポートに関する紹介

バージョン
Deno@2.1.4+9d315f2 (※⚠️ Deno v2.1.5もしくはv2.2.0あたりで導入されると思われる変更に依存しているため、canaryバージョンを使って検証しています)

要約
> ⚠️実験的APIのため、今後、使用方法などが変更される可能性があります
1. Denoの実行時に OTEL_DENO=true --unstable-otel を指定することで、fetchDeno.serveなどのAPIの計装や console.* によるLogsの送信が有効化されます
2. カスタムのTracesMetricsを送信したいときは、Deno公式の@deno/otelパッケージと@opentelemetry/api@opentelemetry/sdk-metricsを併用するとよさそうです
3. 現在、開発が進められているFresh v2向けにもサポートが進められています
追記) 2025/01/12
Deno v2.1.5から@opentelemetry/apiとの連携に@deno/otelの使用が不要になりました ( OTEL_DENO=true --unstable-otel が設定されていれば、自動で連携できます)
javascript
import { trace } from "npm:@opentelemetry/api@^1.9.0"; const tracer = trace.getTracer("example"); function someOperation() { return new Promise((ok) => setTimeout(ok, 3000)); } await tracer.startActiveSpan("someOperation", async (span) => { await someOperation(); span.end(); });
※ただし、Deno v2.1.5で試したところ、 startActiveSpan の実行でエラーが起きてしまうようです

1. OpenTelemetry Collectorの設定をする
Configuration | OpenTelemetryを参考に設定を用意してみます (参考: 執筆時点での最新のドキュメント)
OpenTelemetry Collectorの設定ファイルを用意します
otelcol.yaml
receivers: otlp: protocols: http: endpoint: localhost:4318 exporters: debug: verbosity: detailed service: pipelines: metrics: receivers: [otlp] exporters: [debug] traces: receivers: [otlp] exporters: [debug]
ひとまず疎通の確認をしたいので debug エクスポーターを有効化しておきます
Denoの内部で使われているopentelemetry-rustではデフォルトで http://localhost:4318 向けにSignalsを送信するようなので、それに合わせてエンドポイントの設定をしておきます (opentelemetry-otlp/src/exporter/mod.rs)
deno fmtコマンドはYAMLもサポートしているので、設定ファイルをフォーマットできて便利です
shell
$ deno fmt otelcol.yaml
下記コマンドで設定内容を確認できます
shell
$ otelcol validate --config=otelcol.yaml
OpenTelemetry Collectorを起動します
shell
$ otelcol --config=otelcol.yaml

2. 依存パッケージのダウンロード
必要に応じて依存パッケージをダウンロードします
shell
# 1) カスタムのTracesを送信したい場合 $ deno add jsr:@deno/otel npm:@opentelemetry/api # 2) カスタムのMetricsを送信したい場合 $ deno add npm:@opentelemetry/sdk-metrics

3. Tracesの送信
現状、Denoの内部ではfetchDeno.serveの2つのAPIで計装が行われており、これらのAPIを使うと自動的にバックエンドへTracesが送信されるようです
fetch APIを使ってTracesの送信を試してみます
main.js
await fetch("https://api.github.com/repos/denoland/deno", { headers: { "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", }, });
--unstable-otel 及び OTEL_DENO=true 環境変数を指定して上記スクリプトを実行してみます
shell
$ OTEL_DENO=true deno run --allow-env --allow-net --unstable-otel main.js
OpenTelemetry Collectorに以下のようなログが出力されます
shell
YYYY-MM-DDTHH:mm:ss.SSSZZ info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1} YYYY-MM-DDTHH:mm:ss.SSSZZ info ResourceSpans #0 Resource SchemaURL: Resource attributes: -> process.runtime.version: Str(2.1.4+9d315f2) -> telemetry.sdk.name: Str(deno-opentelemetry) -> telemetry.sdk.language: Str(deno-rust) -> service.name: Str(unknown_service) -> process.runtime.name: Str(deno) -> telemetry.sdk.version: Str(2.1.4+9d315f2-0.27.0) ScopeSpans #0 ScopeSpans SchemaURL: InstrumentationScope deno 2.1.4+9d315f2 Span #0 Trace ID : 828595b8b4a8f6699bd1ec38a47e1eed Parent ID : ID : 21c40683a9c261de Name : GET Kind : Client ... Status code : Unset Status message : Attributes: -> http.request.method: Str(GET) -> url.full: Str(https://api.github.com/repos/denoland/deno) -> url.scheme: Str(https) -> url.path: Str(/repos/denoland/deno) -> url.query: Str() -> http.response.status_code: Str(200) {"kind": "exporter", "data_type": "traces", "name": "debug"}
Deno本体で計装されたAPIからのtracesの送信を無効化したい場合、 OTEL_DENO_TRACING=false によって無効化できそうです (cli/args/flags.rs)

4. Logsの送信
console.* で出力したログは自動的にLogsとしてバックエンドへ送信されます
main.js
console.info("foo");
このスクリプトを実行してみます
shell
$ OTEL_DENO=true deno run --allow-env --unstable-otel main.js foo
すると、OpenTelemetry Collectorで以下のようなログが出力されます
shell
YYYY-MM-DDTHH:mm:ss.SSSZZ info Logs {"kind": "exporter", "data_type": "logs", "name": "debug", "resource logs": 1, "log records": 1} YYYY-MM-DDTHH:mm:ss.SSSZZ info ResourceLog #0 Resource SchemaURL: Resource attributes: -> telemetry.sdk.name: Str(deno-opentelemetry) -> process.runtime.name: Str(deno) -> process.runtime.version: Str(2.1.4+9d315f2) -> telemetry.sdk.version: Str(2.1.4+9d315f2-0.27.0) -> telemetry.sdk.language: Str(deno-rust) -> service.name: Str(unknown_service) ScopeLogs #0 ScopeLogs SchemaURL: InstrumentationScope deno LogRecord #0 ... SeverityText: INFO SeverityNumber: Info(9) Body: Str(foo ) Trace ID: Span ID: Flags: 0 {"kind": "exporter", "data_type": "logs", "name": "debug"}
Logsの送信に関する振る舞いは OTEL_DENO_CONSOLE 環境変数によって調整できるようです (cli/args/flags.rs)
例) OTEL_DENO_CONSOLE=ignore を指定すると、バックエンドへのLogsの送信が無効化されます

5. カスタムのTracesの送信
Deno本体のテストコードを参考にカスタムのTracesを送信してみます ( tests/specs/cli/otel_basic/basic.ts)
@deno/otel@opentelemetry/apiを併用することで送信できます
javascript
import { trace } from "@opentelemetry/api"; import { register } from "@deno/otel"; register(); const tracer = trace.getTracer("example"); function someOperation() { return new Promise((ok) => setTimeout(ok, 3000)); } await tracer.startActiveSpan("someOperation", async (span) => { await someOperation(); span.end(); });
OpenTelemetry Collectorのログから someOperation という名前のSpanが作成されていることを確認できます
shell
YYYY-MM-DDTHH:mm:ss.SSSZZ info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1} YYYY-MM-DDTHH:mm:ss.SSSZZ info ResourceSpans #0 Resource SchemaURL: Resource attributes: -> process.runtime.version: Str(2.1.4+9d315f2) -> telemetry.sdk.version: Str(2.1.4+9d315f2-0.27.0) -> process.runtime.name: Str(deno) -> service.name: Str(unknown_service) -> telemetry.sdk.name: Str(deno-opentelemetry) -> telemetry.sdk.language: Str(deno-rust) ScopeSpans #0 ScopeSpans SchemaURL: InstrumentationScope example Span #0 Trace ID : 771ee0a3832c8040b04e2a883516240c Parent ID : ID : f289545ad405c65e Name : someOperation Kind : Internal ... Status code : Unset Status message : {"kind": "exporter", "data_type": "traces", "name": "debug"}
fetchとの併用
自前で作成したSpan内でDeno内部で計装されているAPI (fetch)を実行した場合、適切に親子関係の紐づけが行われるようです
javascript
import { trace } from "@opentelemetry/api"; import { register } from "@deno/otel"; register(); const tracer = trace.getTracer("example"); await tracer.startActiveSpan("makeHTTPRequest", async (span) => { await fetch("https://api.github.com/repos/denoland/deno", { headers: { "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", }, }); span.end(); });
fetchに対応するSpanParent IDが設定されており、 makeHTTPRequest が参照されています
shell
YYYY-MM-DDTHH:mm:ss.SSSZZ info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 2} YYYY-MM-DDTHH:mm:ss.SSSZZ info ResourceSpans #0 Resource SchemaURL: Resource attributes: -> process.runtime.name: Str(deno) -> telemetry.sdk.version: Str(2.1.4+9d315f2-0.27.0) -> telemetry.sdk.name: Str(deno-opentelemetry) -> process.runtime.version: Str(2.1.4+9d315f2) -> telemetry.sdk.language: Str(deno-rust) -> service.name: Str(unknown_service) ScopeSpans #0 ScopeSpans SchemaURL: InstrumentationScope deno 2.1.4+9d315f2 Span #0 Trace ID : 2a709f3c26099083d36ce9be197c7fd8 Parent ID : f99d09c34da6d3cd ID : bf8ef320a2e9024a Name : GET Kind : Client ... Status code : Unset Status message : Attributes: -> http.request.method: Str(GET) -> url.full: Str(https://api.github.com/repos/denoland/deno) -> url.scheme: Str(https) -> url.path: Str(/repos/denoland/deno) -> url.query: Str() -> http.response.status_code: Str(200) ScopeSpans #1 ScopeSpans SchemaURL: InstrumentationScope example Span #0 Trace ID : 2a709f3c26099083d36ce9be197c7fd8 Parent ID : ID : f99d09c34da6d3cd Name : makeHTTPRequest Kind : Internal ... Status code : Unset Status message : {"kind": "exporter", "data_type": "traces", "name": "debug"}

6. カスタムMetricsの送信
Deno本体のテストコードを参考に、カスタムMetricsを送信してみます (tests/specs/cli/otel_basic/metric.ts)
@opentelemetry/sdk-metrics Deno.telemetry.MetricExporter を併用すると送信できるようです
javascript
import { MeterProvider, PeriodicExportingMetricReader, } from "@opentelemetry/sdk-metrics"; const exporter = new Deno.telemetry.MetricExporter(); const meterProvider = new MeterProvider(); meterProvider.addMetricReader( new PeriodicExportingMetricReader({ exporter, exportIntervalMillis: 1000, }), ); const meter = meterProvider.getMeter("meter"); const views = meter.createCounter("views", { description: "The total number of views of the page", }); views.add(1); await new Promise((ok) => setTimeout(ok, 1500)); views.add(1); await new Promise((ok) => setTimeout(ok, 1500));
OpenTelemetry Collectorの出力内容)
shell
YYYY-MM-DDTHH:mm:ss.SSSZZ info Metrics {"kind": "exporter", "data_type": "metrics", "name": "debug", "resource metrics": 1, "metrics": 1, "data points": 1} YYYY-MM-DDTHH:mm:ss.SSSZZ info ResourceMetrics #0 Resource SchemaURL: ScopeMetrics #0 ScopeMetrics SchemaURL: InstrumentationScope meter Metric #0 Descriptor: -> Name: views -> Description: The total number of views of the page -> Unit: -> DataType: Sum -> IsMonotonic: true -> AggregationTemporality: Cumulative NumberDataPoints #0 ... Value: 1.000000 {"kind": "exporter", "data_type": "metrics", "name": "debug"} YYYY-MM-DDTHH:mm:ss.SSSZZ info Metrics {"kind": "exporter", "data_type": "metrics", "name": "debug", "resource metrics": 1, "metrics": 1, "data points": 1} YYYY-MM-DDTHH:mm:ss.SSSZZ info ResourceMetrics #0 Resource SchemaURL: ScopeMetrics #0 ScopeMetrics SchemaURL: InstrumentationScope meter Metric #0 Descriptor: -> Name: views -> Description: The total number of views of the page -> Unit: -> DataType: Sum -> IsMonotonic: true -> AggregationTemporality: Cumulative NumberDataPoints #0 ... ... Value: 2.000000 {"kind": "exporter", "data_type": "metrics", "name": "debug"} YYYY-MM-DDTHH:mm:ss.SSSZZ info Metrics {"kind": "exporter", "data_type": "metrics", "name": "debug", "resource metrics": 1, "metrics": 1, "data points": 1} YYYY-MM-DDTHH:mm:ss.SSSZZ info ResourceMetrics #0 Resource SchemaURL: ScopeMetrics #0 ScopeMetrics SchemaURL: InstrumentationScope meter Metric #0 Descriptor: -> Name: views -> Description: The total number of views of the page -> Unit: -> DataType: Sum -> IsMonotonic: true -> AggregationTemporality: Cumulative NumberDataPoints #0 ... Value: 2.000000 {"kind": "exporter", "data_type": "metrics", "name": "debug"}

7. Fresh v2でのサポートについて
> ⚠️ Fresh v2はまだアルファバージョンであり、正式リリースは行われていません
Deno本体でのOpenTelemetryサポートを活用して、Fresh v2向けに計装が進められているようです (feat: add open telemetry instrumentation (denoland/fresh#2786))

おわりに
OpenTelemetryのサポートはまだ実験的なので今後どうなるかはわからないのですが、もしかしたらDeno Deployの方にもサポートが入る可能性があるかもしれないので、結構便利な機能なのではないかと思いました

参考

関連ページ