CLOVER🍀

That was when it all began.

OpenTelemetryのNode.jsライブラリーをauto-instrumentations-nodeを使わずに組み込む(トレースのみ)

これは、なにをしたくて書いたもの?

前にNode.jsでOpenTelemetry(トレースのみ)を試してみました。

Node.jsでOpenTelemetryのトレースを試す - CLOVER🍀

この時、OpenTelemetryをアプリケーションに組み込むためのメタパッケージとしてauto-instrumentations-nodeを使ったのですが、すべての
instrumentationライブラリーが含まれるためサイズが大きいという話があります。

気にならないかもしれませんが、仮に個々に使うとしたらどうなるのか?を今回試してみたいと思います。

OpenTelemetry JavaScript Instrumentationを自分で登録する

今回のお題で見るべきページは、こちらになります。

メタパッケージauto-instrumentations-nodeを使う時は、以下のようにしてauto-instrumentations-nodeのregisterスクリプト
Node.jsのオプションで指定します。

$ export NODE_OPTIONS='-r @opentelemetry/auto-instrumentations-node/register'

個々のinstrumentationライブラリーを指定する場合は、こんな感じのコードを作成して同じくNode.jsの-r(または--require)オプションで
指定するようです。

/*instrumentation.js*/
const { HttpInstrumentation } = require("@opentelemetry/instrumentation-http");
const { ExpressInstrumentation } = require("@opentelemetry/instrumentation-express");

const sdk = new NodeSDK({
  ...
  instrumentations: [
    // Express instrumentation expects HTTP layer to be instrumented
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
  ]
});

お題

今回は、こんなお題で試してみたいと思います。

  • Redisを用意する
  • Expressを使ったアプリケーションを作成し、ioredisを使ってRedisへもアクセスする
  • トレースのテレメトリーデータはJaegerに収集する

instrumentationライブラリーの使い方の確認が主テーマなので、今回は簡単な構成にします。

環境

今回の環境は、こちら。

Node.js。

$ node --version
v18.17.1


$ npm --version
9.6.7

Redis。

$ bin/redis-server --version
Redis server v=7.2.1 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64 build=81a2b5148e5873e4

Redisへは172.17.0.2でアクセスするものとし、redis-userpasswordで使えるユーザーを作成しているものとします。

Jaeger。

$ ./jaeger-all-in-one version
2023/09/17 10:50:43 maxprocs: Leaving GOMAXPROCS=8: CPU quota undefined
2023/09/17 10:50:43 application version: git-commit=2d351c3f30072cae7f5755be20e34c2697b9e3b5, git-version=v1.49.0, build-date=2023-09-07T13:13:08Z
{"gitCommit":"2d351c3f30072cae7f5755be20e34c2697b9e3b5","gitVersion":"v1.49.0","buildDate":"2023-09-07T13:13:08Z"}

Jaegerへは、172.17.0.3でアクセスするものとします。

アプリケーションを作成する

それではアプリケーションを作成します。こちらは、TypeScriptで作成しましょう。

Node.jsのプロジェクトの作成。

$ npm init -y
$ npm i -D typescript
$ npm i -D @types/node@v18
$ npm i -D prettier
$ mkdir src

Expressとioredisのインストール。

$ npm i express
$ npm i -D @types/express
$ npm i ioredis

現時点での依存関係。

  "devDependencies": {
    "@types/express": "^4.17.17",
    "@types/node": "^18.17.17",
    "prettier": "^3.0.3",
    "typescript": "^5.2.2"
  },
  "dependencies": {
    "express": "^4.18.2",
    "ioredis": "^5.3.2"
  }

scripts

  "scripts": {
    "build": "tsc --project .",
    "build:watch": "tsc --project . --watch",
    "format": "prettier --write src"
  },

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "moduleResolution": "node",
    "lib": ["esnext"],
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

.prettierrc.json

{
  "singleQuote": true,
  "printWidth": 120
}

簡単なアプリケーションを作成。

src/app.ts

import express from 'express';
import Redis from 'ioredis';

const app = express();
app.use(express.text());

const redis = new Redis({
  host: '172.17.0.2',
  port: 6379,
  username: 'redis-user',
  password: 'password',
  db: 0,
});

app.get('/:id', async (req, res) => {
  const id = req.params['id'];
  const data = await redis.get(id);

  res.contentType('text/plain');
  res.send(data);
});

app.post('/:id', async (req, res) => {
  const id = req.params['id'];
  const body = req.body;

  await redis.set(id, body);

  res.contentType('text/plain');
  res.send(body);
});

const port = 3000;

app.listen(port, () => {
  console.log(`[${new Date().toISOString()}] server startup.`);
});

ビルドすると、distディレクトリ内に結果が出力されるので

$ npm run build

起動。

$ node dist/app.js
[2023-09-17T11:08:49.591Z] server startup.

動作確認。

$ curl -XPOST -H 'Content-Type: text/plain' localhost:3000/foo -d 'Hello World'
Hello World


$ curl -H 'Content-Type: text/plain' localhost:3000/foo
Hello World

OKですね。これでベースのアプリケーションは作成できました。

Node.jsのOpenTelemetry Instrumentationライブラリーを追加する

続いて、OpenTelemetryのinstrumentationライブラリーを追加しましょう。

auto-instrumentations-nodeに含まれているパッケージは以下に記載されています。

https://github.com/open-telemetry/opentelemetry-js-contrib/tree/auto-instrumentations-node-v0.39.2/metapackages/auto-instrumentations-node

今回は、この中から@opentelemetry/instrumentation-express、@opentelemetry/instrumentation-ioredis、@opentelemetry/instrumentation-httpを
使うことにします。

また、@opentelemetry/sdk-nodeも必要になります。

それぞれインストール。

$ npm i @opentelemetry/sdk-node
$ npm i @opentelemetry/instrumentation-express
$ npm i @opentelemetry/instrumentation-ioredis
$ npm i @opentelemetry/instrumentation-http

依存関係は、こうなりました。

  "devDependencies": {
    "@types/express": "^4.17.17",
    "@types/node": "^18.17.17",
    "prettier": "^3.0.3",
    "typescript": "^5.2.2"
  },
  "dependencies": {
    "@opentelemetry/instrumentation-express": "^0.33.1",
    "@opentelemetry/instrumentation-http": "^0.43.0",
    "@opentelemetry/instrumentation-ioredis": "^0.35.1",
    "@opentelemetry/sdk-node": "^0.43.0",
    "express": "^4.18.2",
    "ioredis": "^5.3.2"
  }

次に、アプリケーションに組み込むスクリプトを書いていきます。

OpenTelemetryのドキュメントではJavaScriptで直接書くか、TypeScriptで書いてts-nodeで実行という例になっていますが、今回は
TypeScriptで書いてJavaScriptにビルドして実行したいと思います。

以下の2つを見つつ、

こんなスクリプトを作成。

src/instrumentation.ts

import { NodeSDK } from '@opentelemetry/sdk-node';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';

const sdk = new NodeSDK({
  instrumentations: [new HttpInstrumentation(), new ExpressInstrumentation(), new IORedisInstrumentation()],
});

sdk.start();

NodeSDKインスタンスを作成し、instrumentation等の設定を行い、最後にNodeSDK#startします。

ビルド。

$ npm run build

起動。

$ node dist/app.js
[2023-09-17T11:08:49.591Z] server startup.

環境変数を設定。

$ export OTEL_TRACES_EXPORTER=otlp
$ export OTEL_METRICS_EXPORTER=none
$ export OTEL_LOGS_EXPORTER=none
$ export OTEL_EXPORTER_OTLP_ENDPOINT=http://172.17.0.3:4318
$ export OTEL_NODE_RESOURCE_DETECTORS='env,host,os,process,container'
$ export OTEL_SERVICE_NAME=app
$ export NODE_OPTIONS='-r ./dist/instrumentation.js'

NODE_OPTIONSには、-rオプションで先程作成したスクリプトのビルド結果を指定します。

$ export NODE_OPTIONS='-r ./dist/instrumentation.js'

起動。

$ node dist/app.js
[2023-09-17T12:16:25.041Z] server startup.

また、NODE_OPTIONSを指定せずに以下のような指定でもOKです。

$ node -r ./dist/instrumentation.js dist/app.js

確認。

$ curl -XPOST -H 'Content-Type: text/plain' localhost:3000/foo -d 'Hello World'
Hello World


$ curl -H 'Content-Type: text/plain' localhost:3000/foo
Hello World

JaegerのWeb UI(http://[Jaegerが動作しているホスト]:16686/)にアクセスして、確認してみます。

検索すると、確認した時に記録されたトレースが表示されました。

OKそうですね。

というわけで、こんな感じでセットアップのスクリプトを書けばauto-instrumentations-nodeを使わずともOpenTelemetryによるトレースを
組み込むことができました。

ハマったこと

オマケ的に、ハマったことを書いてみます。

組み込み方がわからない

OpenTelemetryのNode.js instrumentationライブライーを組み込む時に、以下を参考にしたわけですが。

実は、パッケージ個々のページにも組み込み方が書かれています。

これらのページには、以下のようにNodeTracerProviderというものを使う方法が書かれていて、OpenTelemetryのドキュメントと異なるので
どちらが良いのか迷ったのですが。

const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');

const provider = new NodeTracerProvider();
provider.register();

registerInstrumentations({
  instrumentations: [
    // Express instrumentation expects HTTP layer to be instrumented
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
  ],
});

OpenTelemetryのドキュメントの履歴を見ると、NodeTracerProviderを使っていたものからNodeSDKを使ったものに書き直されて
いたので、ドキュメントの方が良さそうですね。

Rework js library instrumentation (#2988) · open-telemetry/opentelemetry.io@6fadff8 · GitHub

しかも、NodeTracerProviderの方だと動きませんでしたし。

@opentelemetry/instrumentation-httpを追加するのを忘れる

トレース結果を見ているとExpressとioredisがあれば良さそうに思ったので、@opentelemetry/instrumentation-httpを追加するのをやめたら
見事に動かなくなりました…。

@opentelemetry/instrumentation-expressのドキュメントを見ると、@opentelemetry/instrumentation-httpが必要なことは書かれて
いるんですよね。

registerInstrumentations({
  instrumentations: [
    // Express instrumentation expects HTTP layer to be instrumented
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
  ],
});
ローカルモジュールを./で指定するのを忘れる

最初、スクリプト-r dist/instrumentation.jsのような指定をしていて

$ node -r dist/instrumentation.js dist/app.js

以下のエラーに悩まされました…。

node:internal/modules/cjs/loader:1080
  throw err;
  ^

Error: Cannot find module 'dist/instrumentation.js'
Require stack:
- internal/preload
    at Module._resolveFilename (node:internal/modules/cjs/loader:1077:15)
    at Module._load (node:internal/modules/cjs/loader:922:27)
    at internalRequire (node:internal/modules/cjs/loader:174:19)
    at Module._preloadModules (node:internal/modules/cjs/loader:1433:5)
    at loadPreloadModules (node:internal/process/pre_execution:598:5)
    at setupUserModules (node:internal/process/pre_execution:117:3)
    at prepareExecution (node:internal/process/pre_execution:108:5)
    at prepareMainThreadExecution (node:internal/process/pre_execution:37:3)
    at node:internal/main/run_main_module:10:1 {
  code: 'MODULE_NOT_FOUND',
  requireStack: [ 'internal/preload' ]
}

Node.js v18.17.1

自分で作成したファイルを指定する時は、./を付けるんでした…。

おわりに

OpenTelemetryのNode.jsライブラリーを、auto-instrumentations-nodeを使わずに組み込んでみました。

できることはできるのですが、OpenTelemetryの各種instrumentatationライブラリーやOpenTelemetryのSDKの知識が相応に求められる感じ
みたいなので、素直にauto-instrumentations-nodeを使うのが良さそうに思います。

凝ったことは、もっと慣れてから…。