OpenTelemetryに触れてみた
概要
オライリーから発売された「オブザーバビリティ・エンジニアリング[1]」は非ベンダー依存を目指したアプリケーションモニタリングの実装方法としてOpenTelemetryを紹介しています。
監視基盤を実装する場合、Datadog[2]やNew Relic[3]などの監視SaaSを利用するかAWSやAzureが提供しているマネージドな監視サービスを利用すると思います。
直近の業務で使う機会はなさそうですが、アプリケーションモニタリングを収集するためのAPI、ライブラリ、エージェントを提供するOpenTelemetryという技術に興味が出てきましたので、概要やデモなどを紹介します。
またOpenTelemetryのドキュメントは開発者向けと運用者向けでスタートガイドが明確に分けられていました。
私は運用側のエンジニアなので運用視点でOpenTelemetryについて書き記します。(開発者は上のリンクからDevを参照してみてください)
OpenTelemetryとは
OpenTelemetryとは分散トレーシングやメトリクスなどのオブザーバビリティデータを収集、処理、エクスポートするためのオープンソースプロジェクトになります。[4]とOpenCensus[5]というプロジェクトを立ち上げました。この2つのプロジェクトはテレメトリーデータを収集してリアルタイムに好きなプログラミング言語でバックエンドに転送できることを目的にライブラリを提供してきました。両グループは2019年にCNCF[6]傘下のOpenTelemetryプロジェクトとして統合されました。
最初モニタリングやオブザーバビリティのオープンソースコミュニティはOpenTracingOpenTelemetryコンポーネント
OpenTelemetryのコンポーネントについていくつか紹介します。
- コレクター
- テレメトリーデータを受信、処理、エクスポートできる非ベンダー依存のプロキシ(サイドカー)です。PrometheusやJaegerなどのデータを受け取って複数のバックエンド先にデータを送信できます。
- スパン
- 特定の操作の始まりから終わりまでをまとめた作業単位。ユーザーが会員サイトにログインする場合、ログイン処理が完了するまでの一連の流れをトレースすることで、ログイン失敗時にどこでエラーが起きたのか特定しやすくなります。
- トレーサー
- サービス内のリクエストなど、実行した操作で何が起こっているのか詳細な情報を含むスパンを生成します。
- コンテキスト伝搬
- 分散トレーシングを可能にする最も重要なコンセプトです。関連したスパン同士を紐づけ、トレースを組み立てます。
テレメトリーの取得
OpenTelemetryは多くの言語で書かれたコードの計装をサポートしています。さらにそれらの言語は直接アプリケーションコードに手を加える必要がなく、自動計装を活用すれば短時間でテレメトリーの収集が可能になります。
2023年4月現在のサポート言語は以下のとおり[7]です。
Language | Traces | Metrics | Logs |
---|---|---|---|
C++ | Stable | Stable | 検証中 |
C#/.NET | Stable | Stable | 検証中 |
Erlang/Elixir | Stable | 検証中 | 検証中 |
Go | Stable | ベータ版 | 未実装 |
Java | Stable | Stable | 検証中 |
JavaScript/TypeScript | Stable | Stable | 開発版 |
PHP | ベータ版 | ベータ版 | アルファ版 |
Python | Stable | Stable | 検証中 |
Ruby | Stable | 未実装 | 未実装 |
Rust | ベータ版 | アルファ版 | 未実装 |
Swift | Stable | 検証中 | 開発中 |
ログに関してはまだどの言語も開発途中といったところのようです。
ここでは試しにTypeScriptによるトレーシングハンズオンを体験してみます。
TypeScriptハンズオン
まずはトレーシング対象となる簡易的なアプリケーションを用意します。
$ npm install --save-dev typescript \
ts-node \
@types/node \
express \
@types/express \
$ ./node_modules/.bin/tsc --init
import express, { Express } from "express";
const PORT: number = parseInt(process.env.PORT || "8080");
const app: Express = express();
app.get("/", (req, res) => {
res.send("Hello World");
});
app.listen(PORT, () => {
console.log(`Listening for requests on http://localhost:${PORT}`);
});
$ ./node_modules/.bin/ts-node ./app.ts
Listening for requests on http://localhost:8080
Hello World
アプリケーションが実行できましたらトレーシングコードを実装します。
$ npm install --save-dev @opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node
import { NodeSDK } from '@opentelemetry/sdk-node';
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
traceExporter: new ConsoleSpanExporter(),
instrumentations: [getNodeAutoInstrumentations()]
});
sdk.start()
$ ./node_modules/.bin/ts-node --require ./tracing.ts app.ts
Listening for requests on http://localhost:8080
何回かhttp://localhost:8080
にアクセスしますとコンソール上にトレーシング情報が出力されます。
{
traceId: '78aaa7b5db4ab17ad10668e53d3aea6e',
parentId: 'bcd528ec1efd529f',
traceState: undefined,
name: 'middleware - query',
id: 'c2db165d1b7289c9',
kind: 0,
timestamp: 1680404455241000,
duration: 37,
attributes: {
'http.route': '/',
'express.name': 'query',
'express.type': 'middleware'
},
status: { code: 0 },
events: [],
links: []
}
{
traceId: '78aaa7b5db4ab17ad10668e53d3aea6e',
parentId: 'bcd528ec1efd529f',
traceState: undefined,
name: 'middleware - expressInit',
id: '41131b313aea88c1',
kind: 0,
timestamp: 1680404455241000,
duration: 54,
attributes: {
'http.route': '/',
'express.name': 'expressInit',
'express.type': 'middleware'
},
status: { code: 0 },
events: [],
links: []
}
{
traceId: '78aaa7b5db4ab17ad10668e53d3aea6e',
parentId: 'bcd528ec1efd529f',
traceState: undefined,
name: 'request handler - /',
id: '001fa0f7ed43abaf',
kind: 0,
timestamp: 1680404455242000,
duration: 5,
attributes: {
'http.route': '/',
'express.name': '/',
'express.type': 'request_handler'
},
status: { code: 0 },
events: [],
links: []
}
{
traceId: '78aaa7b5db4ab17ad10668e53d3aea6e',
parentId: undefined,
traceState: undefined,
name: 'GET /',
id: 'bcd528ec1efd529f',
kind: 1,
timestamp: 1680404455241000,
duration: 1706,
attributes: {
'http.url': 'http://localhost:8080/',
'http.host': 'localhost:8080',
'net.host.name': 'localhost',
'http.method': 'GET',
'http.scheme': 'http',
'http.target': '/',
'http.user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.62',
'http.flavor': '1.1',
'net.transport': 'ip_tcp',
'net.host.ip': '::1',
'net.host.port': 8080,
'net.peer.ip': '::1',
'net.peer.port': 56849,
'http.status_code': 304,
'http.status_text': 'NOT MODIFIED',
'http.route': ''
},
status: { code: 0 },
events: [],
links: []
}
直接アプリケーションコードに改修を加えることなくトレーシング情報を取得できました。
エクスポート
取得したトレースを分析するためにはJaeger[8]やZipkin[9]などの分析ツールにデータをエクスポートする必要があります。OpenTelemetryはOpenTelemetry Protocol(OTLP)と呼ばれるエクスポーターが提供されています。OTLPを使って収集したデータをJaegerに送信してみます。
まずOTLPエクスポーターをインストールします。
$ npm install --save-dev @opentelemetry/exporter-trace-otlp-http
次に先ほど書いたtracing.ts
を以下のように書き換えます。
import * as opentelemetry from '@opentelemetry/sdk-node';
import {
getNodeAutoInstrumentations,
} from "@opentelemetry/auto-instrumentations-node";
import {
OTLPTraceExporter,
} from "@opentelemetry/exporter-trace-otlp-http";
const sdk = new opentelemetry.NodeSDK({
traceExporter: new OTLPTraceExporter({
// optional - default url is http://localhost:4318/v1/traces
// optional - collection of custom headers to be sent with each request, empty by default
headers: {},
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
JaegerのDockerコンテナを実行します。
$ docker run -d --name jaeger \
-e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
-e COLLECTOR_OTLP_ENABLED=true \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
-p 14250:14250 \
-p 14268:14268 \
-p 14269:14269 \
-p 9411:9411 \
jaegertracing/all-in-one:latest
http://localhost:16686/search
にアクセスするとJaegerのUI画面にアクセスできます。
Jaeger画面
トレースの中にスパンが構成されており、トレース情報をクリックすると各スパンの詳細情報がチェックできます。
トレース情報
コレクター
コンポーネントの項目でも説明したとおりOpenTelemetryコレクターはテレメトリーデータの受信、処理、エクスポートの方法についてベンダーにとらわれない実装を提供します。先ほどの言語別の計測ライブラリにあるエクスポーターを使えばコレクターを使ってエクスポートする必要はないと考えます。
OpenTelemetryコレクターはデフォルトでローカルのコレクターエンドポイントを想定しているため本番利用ではなく、開発環境ですぐのテレメトリーが欲しいときにおススメです。こちらにローカルで実行できるデモ環境が用意されていますのでこちらを使ってOpenTelemetryコレクターをハンズオンしてみます。
$ git clone git@github.com:open-telemetry/opentelemetry-collector-contrib.git
$ cd opentelemetry-collector-contrib/examples/demo
$ docker-compose up -d
Dockerコンテナを起動すると下図のデモ環境が立ち上がります。
デモアーキテクチャ
定期的にクライアントサーバーが出もサーバーにHTTPリクエストを送り、スパン情報をOpenTelemetryコレクターが集計し、JaegerやPrometheus[10]などのバックエンドにエクスポートされます。
- Jaeger http://localhost:16686
- Zipkin http://localhost:9411
- Prometheus http://localhost:9090
Jaeger
クライアントからサーバーへのリクエスト数のトレース
Zipkin
特定トレース情報の詳細
Prometheus
クライアントリクエスト数の推移
ちょっとこのハンズオンは触ってみましたが各ツールがどのような役割を果たしているのかわからず、わかったことが少なかったです。3つとも使ったことなかったのでどのように本番利用するのかイメージが見えず、とりあえず表示されている画面をスクショするに留めました。
所感
OpenTelemetryについて色々触ってみました。冒頭で書いたとおり監視基盤の実装はSaaSやパブリッククラウドのマネージドサービスで実装するケースが多いと思いますので、中々自前で構築するということは少ないと思いますがベンダー依存から脱却した監視基盤の構築はこれからのクラウドネイティブアーキテクチャでは大事ですので、面白い学びになりました。こちらは架空のWebサイトにOpenTelemetryを実装してみたデモ環境ですので、より実践的なOpenTelemetryの実装について知りたい人は是非体験してみてください。
参考文献
Discussion