今後OpenTelemetryを語る上での定番書『入門OpenTelemetry』
オライリー『Learning OpenTelemetry』の邦訳が、オライリージャパンから『入門OpenTelemetry』として刊行された。
縁があって本書の翻訳レビューに関わったが、本書の内容はオブザーバビリティの事実上標準技術である「OpenTelemetry」の概要を掴むのに最適だ。
OpenTelemetryはCNCF(Cloud Native Computing Foundation)のincubatingプロジェクト。オープンソース、ベンダー非依存な体制で、テレメトリの収集・送信の標準と、それを扱うためのフレームワークを提供している。メトリック、ログ、トレース、プロファイルなどのテレメトリシグナルを手広く仕様およびリファレンスライブラリ実装としてカバーし、共通する情報の持ち方を定めることで、それぞれのシグナルがばらばらになることを防ぎ、関係性を持ってオブザーバビリティプラットフォームで解析できるようになる。
逆に、シグナルをどう解析するか、どうビジュアライズするか、どうアラートにつなげるかというのはOpenTelemetryプロジェクトの責任範疇ではなく、シグナルを最終的に受け取るオブザーバビリティプラットフォームに委ねられている。
OpenTelemetryプロジェクトの共同設立者たちの手による、「公式」とも呼べそうな本書は、技術詳細・コードには深入りしすぎない範囲で、OpenTelemetryがオブザーバビリティにもたらす価値と世界観を概説している。
章構成は以下のとおり。
- 1章 最新のオブザーバビリティの現状
- 2章 なぜOpenTelemetryを使うのか
- 3章 OpenTelemetry概要
- 4章 OpenTelemetryのアーキテクチャ
- 5章 アプリケーションの計装
- 6章 ライブラリの計装
- 7章 インフラストラクチャの観測
- 8章 テレメトリーパイプラインの設計
- 9章 オブザーバビリティの展開
- 付録A OpenTelemetryプロジェクト
- 付録B さらなる資料
コンパクト(212ページ)で1章ごとに要点がまとまっており、退屈せずにすらすらと読むことができるだろう。
導入の第1〜3章では、OpenTelemetryがオブザーバビリティ界隈においてなぜ重要なファクターとなってきているのかを知ることができる。オブザーバビリティに何らかの形で関わっているのなら、この3章ぶんだけでも読む価値はある。
印象的なページとして、第1章で図示される「シグナルの三つ編み」(サイロ化された柱ではなく相互に組み合わせながら洞察する)は、「オブザーバビリティの3本柱」と見聞きするたびに自身がなんとなくしっくりこないと感じていたところに、実に膝を打つ表現だった。
大袈裟に言えば、本書の刊行後は、オブザーバビリティ界隈において柱という表現から編みの表現にトレンドが移るのではないかとさえ思う(原書は昨年に刊行しており、そんなに海外製品の表現が変わった気もしないので、思い込み強すぎ説はもちろんある)。
第4〜7章では、アプリケーションエンジニアやSRE、インフラエンジニアが、サービスを構成するアプリケーションやインフラにOpenTelemetryの計装をする手法を概説している。深入りしない範囲で、それぞれのレイヤーでの計装手段、注意事項、得られるメリットがまとまっており、必要なところだけを拾い読みしてもいいだろう。
実際のところ、OpenTelemetryのSDK、ライブラリなどはまだ安定していない印象がある(全般にトレース実装については各所充実・安定的に見えるが)。このあたりは当面、各種ドキュメントやライブラリの開発プロジェクトの動向をウォッチして変化に追従していくしかなさそうだ。
第8章は毛色を少し変えて、テレメトリを受け取って処理するパイプラインを説明している。テレメトリを欠落させないためのコレクタの構成、フィルタリングとサンプリング、変換、PIIのような機微データの処理などが語られる。
第8章の以下の文はとても好きだ。
トレースは、エラーやタイムアウトにつながるすべてのイベントを簡単に見つけられる、豊富で、よく整理されたログであることを忘れないでください。ログをサンプリングすることが悪いことのように思えるのなら、なぜトレースをサンプリングしたいのでしょうか。
最後の第9章では、組織・ビジネスにオブザーバビリティおよびOpenTelemetryエコシステム導入がもたらす価値を説明し、導入計画をどう進めていくかのチェックリストを提示する。
「私たちは、OpenTelemetryの導入を成功させるためには、一連の基本的な行動が必要であることを長年にわたって見出してきました」という著者の言から始まる、「管理職は関与しているか」「小さくても重要な最初の目標を設定しましたか」など7つからなるチェックリストは、オブザーバビリティ導入オンボーディングに借用できるなと思った。
オブザーバビリティ方面の書籍としてはこちらもオライリージャパンの『オブザーバビリティ・エンジニアリング』も良い本だが、まずは本書を読んでからのほうが、挫折せずに理解できるだろう。
本書の翻訳レビュー行為自体、自分自身の知見を広げるのにとても役立ち、その後の活動に大きく活かせているので最高体験だった。機会をいただいたことに感謝。
OpenTelemetryのzero-code計装を試している〜その4。Python
今日はPythonでのzero-code計装をやってみる。
kmuto.hatenablog.com kmuto.hatenablog.com kmuto.hatenablog.com
いくつか罠があるようなのだが、とりあえず今見たところではopentelemetry-pythonもopentelemetry-python-contribも頻繁にアップデートされており、かつ対象のわかっている小さな範囲では問題なさそうだった。
モンキーパッチを使って挙動に割り込む方法が採用されている。
まずはいつものように雑なWebサーバーを立てよう。わかってはいたがhttp.server
モジュールは対象外(サポートレジストリ)なので、Aiohttpを使うことにした。
計装パッケージとともにrequirements.txt
でインストール対象にする。
aiohttp opentelemetry-distro opentelemetry-exporter-otlp
pip3
でインストールしたあと(Debian GNU/Linux上だとPythonパッケージ関連が厄介めなのでvenv
で環境作った)、さらにopentelemetry-bootstrap -a install
で計装ライブラリ一式をダウンロードする。
pip3 install -r requirements.txt opentelemetry-bootstrap -a install
適当Webアプリケーションhello-server.py
を書く。
from aiohttp import web app = web.Application() app.add_routes([web.get('/', lambda _: web.Response(text='Hello, world!')), web.get('/error', lambda _: 1/0), web.get('/500', lambda _: web.Response(status=500, text='Internal Server Error'))]) web.run_app(app, port=5000)
Pythonで適当ランタイム例外どう出すんだっけ、と悩んだけど、雑に0除算した(ひどい)。
python3 hello-server.py
で実行して、curlから叩いてみる。
$ curl http://localhost:5000 Hello, world! $ curl http://localhost:5000/error 500 Internal Server Error Server got itself in trouble $ curl http://localhost:5000/500 Internal Server Error
よさそうだ。
OpenTelemetry Collectorをいつものように用意し、実行しておく。
receivers: otlp: protocols: http: exporters: debug: verbosity: detailed service: pipelines: metrics: receivers: [otlp] exporters: [debug] logs: receivers: [otlp] exporters: [debug] traces: receivers: [otlp] exporters: [debug]
では、zero-code計装を実行してみる。opentelemetry-instrumentというコマンドがzero-code計装ラッパーになっている。
opentelemetry-instrument --service_name python-zerocode python3 hello-server.py
環境変数を使ってもよい。
OTEL_SERVICE_NAME=python-zerocode opentelemetry-instrument python3 hello-server.py
curlを叩いて、トレースが送られる様子を見る。
2025-01-21T21:40:28.706+0900 info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1} 2025-01-21T21:40:28.706+0900 info ResourceSpans #0 Resource SchemaURL: Resource attributes: -> telemetry.sdk.language: Str(python) -> telemetry.sdk.name: Str(opentelemetry) -> telemetry.sdk.version: Str(1.29.0) -> service.name: Str(python-zerocode) -> telemetry.auto.version: Str(0.50b0) ScopeSpans #0 ScopeSpans SchemaURL: InstrumentationScope opentelemetry.instrumentation.aiohttp_server Span #0 Trace ID : b035a7acc1fbe6a2eb4d76dfbfc7688e Parent ID : ID : c6d834325bea6982 Name : / Kind : Server Start time : 2025-01-21 12:40:26.005757581 +0000 UTC End time : 2025-01-21 12:40:26.005991817 +0000 UTC Status code : Unset Status message : Attributes: -> http.scheme: Str(http) -> http.host: Str(localhost) -> net.host.port: Int(5000) -> http.route: Str(<lambda>) -> http.flavor: Str(1.1) -> http.target: Str(/) -> http.url: Str(http://localhost:5000/) -> http.method: Str(GET) -> http.server_name: Str(localhost:5000) -> http.user_agent: Str(curl/7.88.1) -> http.status_code: Int(200) {"kind": "exporter", "data_type": "traces", "name": "debug"} 2025-01-21T21:40:38.706+0900 info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 2} 2025-01-21T21:40:38.706+0900 info ResourceSpans #0 Resource SchemaURL: Resource attributes: -> telemetry.sdk.language: Str(python) -> telemetry.sdk.name: Str(opentelemetry) -> telemetry.sdk.version: Str(1.29.0) -> service.name: Str(python-zerocode) -> telemetry.auto.version: Str(0.50b0) ScopeSpans #0 ScopeSpans SchemaURL: InstrumentationScope opentelemetry.instrumentation.aiohttp_server Span #0 Trace ID : b00c92c749823cfe5bfbbdd3bb82a5e2 Parent ID : ID : 58ba38395d6a83ca Name : /error Kind : Server Start time : 2025-01-21 12:40:34.303458872 +0000 UTC End time : 2025-01-21 12:40:34.304187316 +0000 UTC Status code : Error Status message : ZeroDivisionError: division by zero Attributes: -> http.scheme: Str(http) -> http.host: Str(localhost) -> net.host.port: Int(5000) -> http.route: Str(<lambda>) -> http.flavor: Str(1.1) -> http.target: Str(/error) -> http.url: Str(http://localhost:5000/error) -> http.method: Str(GET) -> http.server_name: Str(localhost:5000) -> http.user_agent: Str(curl/7.88.1) Events: SpanEvent #0 -> Name: exception -> Timestamp: 2025-01-21 12:40:34.304175408 +0000 UTC -> DroppedAttributesCount: 0 -> Attributes:: -> exception.type: Str(ZeroDivisionError) -> exception.message: Str(division by zero) -> exception.stacktrace: Str(Traceback (most recent call last): File ".../lib/python3.11/site-packages/opentelemetry/trace/__init__.py", line 589, in use_span yield span File ".../lib/python3.11/site-packages/opentelemetry/sdk/trace/__init__.py", line 1104, in start_as_current_span yield span File ".../lib/python3.11/site-packages/opentelemetry/instrumentation/aiohttp_server/__init__.py", line 230, in middleware resp = await handler(request) ^^^^^^^^^^^^^^^^^^^^^^ File ".../python-zerocode/lib/python3.11/site-packages/aiohttp/web_urldispatcher.py", line 208, in handler_wrapper result = old_handler(request) # type: ignore[call-arg] ^^^^^^^^^^^^^^^^^^^^ File ".../hello-server.py", line 5, in <lambda> web.get('/error', lambda _: 1/0), ~^~ ZeroDivisionError: division by zero ) -> exception.escaped: Str(False) Span #1 Trace ID : f434c991c1933b82f576d2ff73eed4ad Parent ID : ID : efde73433bf5aef4 Name : /500 Kind : Server Start time : 2025-01-21 12:40:36.371515683 +0000 UTC End time : 2025-01-21 12:40:36.37165803 +0000 UTC Status code : Error Status message : Attributes: -> http.scheme: Str(http) -> http.host: Str(localhost) -> net.host.port: Int(5000) -> http.route: Str(<lambda>) -> http.flavor: Str(1.1) -> http.target: Str(/500) -> http.url: Str(http://localhost:5000/500) -> http.method: Str(GET) -> http.server_name: Str(localhost:5000) -> http.user_agent: Str(curl/7.88.1) -> http.status_code: Int(500) {"kind": "exporter", "data_type": "traces", "name": "debug"}
Exceptionイベントが記録されたのはPythonが初めてだな。リッチなエラーバックトレースが書き出された(ほかの言語でも0除算みたいなことをすればもしかして何か出るのかな。コンパイラだますのが面倒だが)。
また、Aiohttp対象の場合はメトリックとしてhttp.server.duration, http.server.active_requestsのヒストグラムが出てくるようだ。
OpenTelemetryのzero-code計装を試している〜その3。Java/Kotlin
Go、.NETときて、今回はJavaでのzero-code計装のトライである。
kmuto.hatenablog.com kmuto.hatenablog.com
公式サイトによれば、Java Agentを使う方法と、Spring Boot starterを使う方法がある。
まずはJava Agentで試すことにした。
Java Agentの場合、シンプルにopentelemetry-javaagent.jarをエージェント組み込みするだけでJVMから情報をひっぱってきてくれる。
java -javaagent:path/to/opentelemetry-javaagent.jar -Dotel.service.name=your-service-name -jar myapp.jar
なるほど簡単。引数のほか環境変数、プロパティファイルで指定することもできる。
で、さっそく素から立てやすいWebサーバーとしてsun.net.httpserver.HttpServer
を使って立てたのだが…JVMのメトリックは飛ぶものの、トレースが全然送られない。
サポートライブラリの対象になかった。せやな…。
標準ライブラリ範疇だとJava Http Clientやloggingあたりの対応があるのだが、Webサーバーだと結局KtorかSpringになるようだ。
Javaと一言で表すには大掛かりになってしまうものの、KotlinのKtorで試すことにする。
Kotlinは昔書籍制作で検証するのに使ったくらいで、完全に忘却。とはいえ、公式サイトを見ながら適当にやったらできた。
Project Generatorからktor-sample.zipをダウンロードし、展開する。
src/main/kotlin/Routing.kt
に/500
と/error
のハンドラを追加した。
package com.example import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.server.routing.* fun Application.configureRouting() { routing { get("/") { call.respondText("Hello World!") } get("/500") { call.respondText("Error", status = io.ktor.http.HttpStatusCode.InternalServerError) } get("/error") { throw RuntimeException("Runtime Error") } } }
ビルドして、正常に動作するか試す。
./gradlew build java -jar build/libs/ktor-sample-all.jar
ポート8080で動いているので、curlから呼び出し。
$ curl http://localhost:8080 Hello World! $ curl http://localhost:8080/error (何も出ないけどサーバー側は例外が出ている) $ curl http://localhost:8080/500 Error
いつものようにOpenTelemetry Collectorを実行しておく。
receivers: otlp: protocols: http: exporters: debug: verbosity: detailed service: pipelines: traces: receivers: [otlp] exporters: [debug]
ではzero-code計装を注入する。
java -javaagent:./opentelemetry-javaagent.jar -Dotel.service.name=kotlin-zerocode -jar build/libs/ktor-sample-all.jar
これで再度curlを試してみると、トレースが出てくる。
2025-01-19T17:50:04.119+0900 info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1} 2025-01-19T17:50:04.119+0900 info ResourceSpans #0 Resource SchemaURL: https://opentelemetry.io/schemas/1.24.0 Resource attributes: -> host.arch: Str(amd64) -> host.name: Str(...) -> os.description: Str(Linux 6.1.0-30-amd64) -> os.type: Str(linux) -> process.command_args: Slice(["/usr/lib/jvm/java-17-openjdk-amd64/bin/java","-javaagent:./opentelemetry-javaagent.jar","-Dotel.service.name=kotlin-zerocode","-jar","build/libs/ktor-sample-all.jar"]) -> process.executable.path: Str(/usr/lib/jvm/java-17-openjdk-amd64/bin/java) -> process.pid: Int(349907) -> process.runtime.description: Str(Debian OpenJDK 64-Bit Server VM 17.0.13+11-Debian-2deb12u1) -> process.runtime.name: Str(OpenJDK Runtime Environment) -> process.runtime.version: Str(17.0.13+11-Debian-2deb12u1) -> service.instance.id: Str(0da10240-547e-4a45-b28c-6133d3ffe1b5) -> service.name: Str(kotlin-zerocode) -> telemetry.distro.name: Str(opentelemetry-java-instrumentation) -> telemetry.distro.version: Str(2.12.0) -> telemetry.sdk.language: Str(java) -> telemetry.sdk.name: Str(opentelemetry) -> telemetry.sdk.version: Str(1.46.0) ScopeSpans #0 ScopeSpans SchemaURL: InstrumentationScope io.opentelemetry.netty-4.1 2.12.0-alpha Span #0 Trace ID : 287f6d7cab81f813910f909ac1ff0570 Parent ID : ID : aac3e33359775de7 Name : GET / Kind : Server Start time : 2025-01-19 08:50:03.077627907 +0000 UTC End time : 2025-01-19 08:50:03.126250246 +0000 UTC Status code : Unset Status message : Attributes: -> network.peer.address: Str(127.0.0.1) -> server.address: Str(localhost) -> client.address: Str(127.0.0.1) -> url.path: Str(/) -> server.port: Int(8080) -> http.request.method: Str(GET) -> thread.id: Int(35) -> http.response.status_code: Int(200) -> http.route: Str(/) -> user_agent.original: Str(curl/7.88.1) -> network.peer.port: Int(34344) -> network.protocol.version: Str(1.1) -> url.scheme: Str(http) -> thread.name: Str(eventLoopGroupProxy-3-1) {"kind": "exporter", "data_type": "traces", "name": "debug"} 2025-01-19T17:50:39.123+0900 info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1} 2025-01-19T17:50:39.123+0900 info ResourceSpans #0 Resource SchemaURL: https://opentelemetry.io/schemas/1.24.0 Resource attributes: -> host.arch: Str(amd64) -> host.name: Str(...) -> os.description: Str(Linux 6.1.0-30-amd64) -> os.type: Str(linux) -> process.command_args: Slice(["/usr/lib/jvm/java-17-openjdk-amd64/bin/java","-javaagent:./opentelemetry-javaagent.jar","-Dotel.service.name=kotlin-zerocode","-jar","build/libs/ktor-sample-all.jar"]) -> process.executable.path: Str(/usr/lib/jvm/java-17-openjdk-amd64/bin/java) -> process.pid: Int(349907) -> process.runtime.description: Str(Debian OpenJDK 64-Bit Server VM 17.0.13+11-Debian-2deb12u1) -> process.runtime.name: Str(OpenJDK Runtime Environment) -> process.runtime.version: Str(17.0.13+11-Debian-2deb12u1) -> service.instance.id: Str(0da10240-547e-4a45-b28c-6133d3ffe1b5) -> service.name: Str(kotlin-zerocode) -> telemetry.distro.name: Str(opentelemetry-java-instrumentation) -> telemetry.distro.version: Str(2.12.0) -> telemetry.sdk.language: Str(java) -> telemetry.sdk.name: Str(opentelemetry) -> telemetry.sdk.version: Str(1.46.0) ScopeSpans #0 ScopeSpans SchemaURL: InstrumentationScope io.opentelemetry.netty-4.1 2.12.0-alpha Span #0 Trace ID : 5b2a39ea9e9b83b2695d6f537901a5b8 Parent ID : ID : 1009964c6a9cba86 Name : GET /error Kind : Server Start time : 2025-01-19 08:50:38.102411829 +0000 UTC End time : 2025-01-19 08:50:38.112710928 +0000 UTC Status code : Error Status message : Attributes: -> network.peer.address: Str(127.0.0.1) -> server.address: Str(localhost) -> client.address: Str(127.0.0.1) -> url.path: Str(/error) -> error.type: Str(500) -> server.port: Int(8080) -> http.request.method: Str(GET) -> thread.id: Int(40) -> http.response.status_code: Int(500) -> http.route: Str(/error) -> user_agent.original: Str(curl/7.88.1) -> network.peer.port: Int(60186) -> network.protocol.version: Str(1.1) -> url.scheme: Str(http) -> thread.name: Str(eventLoopGroupProxy-3-2) {"kind": "exporter", "data_type": "traces", "name": "debug"} 2025-01-19T17:50:44.124+0900 info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1} 2025-01-19T17:50:44.124+0900 info ResourceSpans #0 Resource SchemaURL: https://opentelemetry.io/schemas/1.24.0 Resource attributes: -> host.arch: Str(amd64) -> host.name: Str(...) -> os.description: Str(Linux 6.1.0-30-amd64) -> os.type: Str(linux) -> process.command_args: Slice(["/usr/lib/jvm/java-17-openjdk-amd64/bin/java","-javaagent:./opentelemetry-javaagent.jar","-Dotel.service.name=kotlin-zerocode","-jar","build/libs/ktor-sample-all.jar"]) -> process.executable.path: Str(/usr/lib/jvm/java-17-openjdk-amd64/bin/java) -> process.pid: Int(349907) -> process.runtime.description: Str(Debian OpenJDK 64-Bit Server VM 17.0.13+11-Debian-2deb12u1) -> process.runtime.name: Str(OpenJDK Runtime Environment) -> process.runtime.version: Str(17.0.13+11-Debian-2deb12u1) -> service.instance.id: Str(0da10240-547e-4a45-b28c-6133d3ffe1b5) -> service.name: Str(kotlin-zerocode) -> telemetry.distro.name: Str(opentelemetry-java-instrumentation) -> telemetry.distro.version: Str(2.12.0) -> telemetry.sdk.language: Str(java) -> telemetry.sdk.name: Str(opentelemetry) -> telemetry.sdk.version: Str(1.46.0) ScopeSpans #0 ScopeSpans SchemaURL: InstrumentationScope io.opentelemetry.netty-4.1 2.12.0-alpha Span #0 Trace ID : b3d80bd3e7474a883e1213c4688219df Parent ID : ID : efb2d40bf565f9f9 Name : GET /500 Kind : Server Start time : 2025-01-19 08:50:40.462618021 +0000 UTC End time : 2025-01-19 08:50:40.464162334 +0000 UTC Status code : Error Status message : Attributes: -> network.peer.address: Str(127.0.0.1) -> server.address: Str(localhost) -> client.address: Str(127.0.0.1) -> url.path: Str(/500) -> error.type: Str(500) -> server.port: Int(8080) -> http.request.method: Str(GET) -> thread.id: Int(42) -> http.response.status_code: Int(500) -> http.route: Str(/500) -> user_agent.original: Str(curl/7.88.1) -> network.peer.port: Int(56010) -> network.protocol.version: Str(1.1) -> url.scheme: Str(http) -> thread.name: Str(eventLoopGroupProxy-3-3) {"kind": "exporter", "data_type": "traces", "name": "debug"}
.NETとだいたい同じ雰囲気だね。
Collectorでメトリックも受け付けを有効にしてみたところ、HTTP request duration、送ったスパンの数、JVMリソース状況などがメトリックとして送出されてきていた(Goや.NETもメトリックを送ってきていたんだけどそういえばちゃんと見ていなかったな)。
様子を見たいときにはOpenTelemetry Collectorの設定を以下のように変えればよい。
service: pipelines: metrics: receivers: [otlp] exporters: [debug] logs: receivers: [otlp] exporters: [debug] traces: receivers: [otlp] exporters: [debug]
Springもそのうち試そうと思うが、まずはほかの言語をひととおり見てからにする。
いまどきJavaで書くには何らかのフレームワークを使っているだろうから、Java Agentによるzero-code計装は.NET同様に手軽だし便利そうだなと感じた。
残りはPHP、Python、JavaScriptか〜
ふらっと見てたらこんなissueがあった。