Node.jsのCLIにOpenTelemetryをいれてみる

こんにちは。 Legalscape コンテンツバリューチームの清水です。

「推測するな、計測せよ」という言葉はパフォーマンス改善の文脈で良く用いられるフレーズですが、まさにAIがプログラミングする時代に適した考え方だと思います。

自分もAIを使った開発をする中でAIにパフォーマンス改善させる方法をどうしたらいいかと思ってはいたのですが、簡単に実践できそうな記事があったので、弊社でよく使われているNode.js / TypeScriptで簡単なCLIを実装して試してみました。

blog.kengo-toda.jp

記事の概要

上記の記事はCLIにおいてOpenTelemetryでtracesとeventsをexportすることで、JaegerでそれらをJSONとしてview/downloadでき結果をそのままファイルに残せるし、スクリーンショットを添付することで人間にもわかりやすい形で表示できるようになる、という内容でした。

opentelemetry.io

www.jaegertracing.io

実装

Node.jsではOpenTelemetry公式によるライブラリが提供されています。存在は知っているぐらいだったので、実際どう使うのかを見ていきました。

opentelemetry.io

SDKの初期化

計測を開始する前にSDKの初期化が必要です。

import { resourceFromAttributes } from "@opentelemetry/resources";
import {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} from "@opentelemetry/semantic-conventions";
import { NodeSDK } from "@opentelemetry/sdk-node";

const sdk = new NodeSDK({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: "otel-sample",
    [ATTR_SERVICE_VERSION]: "1.0",
  }),
  traceExporter: new OTLPTraceExporter({
    url: new URL("/v1/traces", endpoint).toString(),
  }),
});
sdk.start();

tracerの取得

先程指定したresourceの名前とVERSIONでtracerを取得できます。

const tracer = trace.getTracer("otel-sample", "1.0");

Spanの作成

tracer.startActiveSpan を使うことでspanを作成することができ、setAttribute, setStatus, recordExceptionのようなメソッドを使って情報を付加できます。

tracer.startActiveSpan('spanName', (span: Span) => {
  span.setAttribute('some attribute', "some value");
  try {
    // impl
  } catch (ex) {
    // エラーのときは
    if (ex instanceof Error) {
      span.recordException(ex);
    }
    span.setStatus({ code: SpanStatusCode.ERROR });
    throw ex;
  } finally {
    span.end();
  }
  return result;
});

これはネストして使えるのですが、並列したspanを作りたいときは

const span1 = tracer.startSpan('work-1');
// do some work
const span2 = tracer.startSpan('work-2');
// do some more work
const span3 = tracer.startSpan('work-3');
// do even more work

span1.end();
span2.end();
span3.end();

のようにすることでTypeScriptのスコープにとらわれずスパンを作成できます。

Span Event

spanに対してメッセージ/ログを紐づけることができSpan Eventといいます。

span.addEvent('some log', {
  'log.severity': 'error',
  'log.message': 'Data not found',
  'request.id': '12345',
});

後の解説するjaeger上でも確認できました。

Jaegerで確認する

上記を使って与えた引数の人たちに挨拶をするCLIを作成しました。

otel-hello · GitHub

このCLIで --otel を指定することでjaegerで計測が確認できることを確かめました。

docker run --rm --name jaeger -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one

Dockerで起動した状態でotelオプションをつけて実行することでUI上でトレースを確認できました。

$ npx tsx src/main.ts --otel http://localhost:4318/ Alice Bob Charlie
Hello, Alice!
Hello, Bob!
Hello, Charlie!
Failed to hello Charlie
Done!

右上のメニューアイコンから Trace JSON をクリックすることで、JSONとして確認することもできました。

ここから

Claude Codeに投げると正しく情報を渡すこともできました。

## トレース解説

サービス `otel-sample` の OpenTelemetry トレースで、`Alice, Bob, Charlie` の3名に挨拶する CLI ツールの実行記録。

### スパンツリー構造

cli-run (2.71s) ── ルートスパン
├── greets (2.71s) ── ERROR
│   ├── greet:Alice (0.95s) ── OK
│   ├── greet:Bob (0.65s) ── OK
│   └── greet:Charlie (1.11s) ── ERROR
└── finish (0.1ms) ── OK

### 処理の流れ

1. **cli-run** がルートスパン。引数 `Alice, Bob, Charlie` を受け取る
2. **greets** が3人分の挨拶を順次(直列)実行する
   - Alice → 完了後に Bob → 完了後に Charlie の順(startTime から判定可能)
3. **finish** は greets 完了後に呼ばれる後処理

### エラーの原因

`greet:Charlie` が **1.10秒** かかり、閾値(おそらく1秒)を超えたため `Too slow: 1.10s` で ERROR になった。このエラーが親スパン `greets` にも伝播して `Some greetings failed` というステータスになっている。

- Alice: 0.95s → OK(ギリギリ)
- Bob: 0.65s → OK
- Charlie: 1.10s → ERROR(閾値超過)

各 `greet:*` スパンにはランダムな `delay_sec` が記録されており、ランダム遅延を入れてスロー処理をシミュレートしている。

### 実行環境

- Node.js 22.13.1 / arm64 Mac
- tsx 経由で `src/main.ts` を直接実行
- OTel エクスポート先: `http://localhost:4318/`(OTLP HTTP)
- Jaeger で可視化

正しく解析できていそうです。今回はサンプルアプリでしたがプロダクトにも取り込んでコーディングエージェントで活用していきたいです。

LegalscapeではAIを活用した開発にも積極的に取り組んでいます。弊社に興味が湧いた方がいればぜひお気軽にご連絡ください。