Location via proxy:   
[Report a bug]   [Manage cookies]                

kmuto’s blog

はてな社でMackerel CREをやっています。料理と旅行といろんなIT技術

今後OpenTelemetryを語る上での定番書『入門OpenTelemetry』

オライリー『Learning OpenTelemetry』の邦訳が、オライリージャパンから『入門OpenTelemetry』として刊行された。

www.oreilly.co.jp

縁があって本書の翻訳レビューに関わったが、本書の内容はオブザーバビリティの事実上標準技術である「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本柱」と見聞きするたびに自身がなんとなくしっくりこないと感じていたところに、実に膝を打つ表現だった。

『入門OpenTelemetry』図1-3より

大袈裟に言えば、本書の刊行後は、オブザーバビリティ界隈において柱という表現から編みの表現にトレンドが移るのではないかとさえ思う(原書は昨年に刊行しており、そんなに海外製品の表現が変わった気もしないので、思い込み強すぎ説はもちろんある)。

第4〜7章では、アプリケーションエンジニアやSRE、インフラエンジニアが、サービスを構成するアプリケーションやインフラにOpenTelemetryの計装をする手法を概説している。深入りしない範囲で、それぞれのレイヤーでの計装手段、注意事項、得られるメリットがまとまっており、必要なところだけを拾い読みしてもいいだろう。

実際のところ、OpenTelemetryのSDK、ライブラリなどはまだ安定していない印象がある(全般にトレース実装については各所充実・安定的に見えるが)。このあたりは当面、各種ドキュメントやライブラリの開発プロジェクトの動向をウォッチして変化に追従していくしかなさそうだ。

第8章は毛色を少し変えて、テレメトリを受け取って処理するパイプラインを説明している。テレメトリを欠落させないためのコレクタの構成、フィルタリングとサンプリング、変換、PIIのような機微データの処理などが語られる。

第8章の以下の文はとても好きだ。

トレースは、エラーやタイムアウトにつながるすべてのイベントを簡単に見つけられる、豊富で、よく整理されたログであることを忘れないでください。ログをサンプリングすることが悪いことのように思えるのなら、なぜトレースをサンプリングしたいのでしょうか。

最後の第9章では、組織・ビジネスにオブザーバビリティおよびOpenTelemetryエコシステム導入がもたらす価値を説明し、導入計画をどう進めていくかのチェックリストを提示する。

「私たちは、OpenTelemetryの導入を成功させるためには、一連の基本的な行動が必要であることを長年にわたって見出してきました」という著者の言から始まる、「管理職は関与しているか」「小さくても重要な最初の目標を設定しましたか」など7つからなるチェックリストは、オブザーバビリティ導入オンボーディングに借用できるなと思った。

オブザーバビリティ方面の書籍としてはこちらもオライリージャパンの『オブザーバビリティ・エンジニアリング』も良い本だが、まずは本書を読んでからのほうが、挫折せずに理解できるだろう。

www.oreilly.co.jp

本書の翻訳レビュー行為自体、自分自身の知見を広げるのにとても役立ち、その後の活動に大きく活かせているので最高体験だった。機会をいただいたことに感謝。

kmuto.hatenablog.com

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のヒストグラムが出てくるようだ。