🐥

opentelemetry-instrumentation-fastapi (python) からOpenTelemetryに入門する

2022/12/06に公開

この投稿は OpenTelemetry Advent Calendar 2022 の 7 日目の記事です。

この記事は筆者が下記を行い、OpenTelemetry に入門することを目的にまとめた記事となっています。

  • 具体的な実装をベースに OpenTelemetry の全体像を掴む
    • FastAPI ベースのアプリケーションで出力した Trace を Google Cloud Trace に export するところまでスコープとして実装する
  • opentelemetry-instrumentation-fastapi の中身を見てどのような機能を提供しているのかを把握する

実装したコードは下記の GitHub repository に置いています。
https://github.com/taxintt/fastapi-with-otel

注意事項

記事の分量上、詳細な説明を省略している箇所がありますがご了承ください。

例えば、OpenTelemetry が誕生した背景などについては、この記事からは省いています。
(気になる方は、下記のリンク先をご参照ください。)
https://opentelemetry.io/docs/concepts/what-is-opentelemetry/

また、今回は Traces に絞って実装を行なっているので、Metrics, Logsの実装は登場しませんので、こちらに関してもご了承下さい。

OpenTelemetryの全体像

最初に OpenTelemetry の全体像を掴むために、ドキュメントを読んでいきます。
全体像をイメージとして把握するには、下記のリンク先にある画像が参考になりそうです。

https://opentelemetry.io/docs/

otel_overview

OpenTelemetry は、Observability frameworkとして表現されるように、telemetry data の収集や backend への転送などの必要な一連の機能を提供します。

OpenTelemetry, also known as OTel for short, is a vendor-neutral open-source Observability framework for instrumenting, generating, collecting, and exporting telemetry data such as traces, metrics, logs.

instrumenting

https://opentelemetry.io/docs/concepts/instrumenting/

前提として、対象のアプリケーションに対して計装 (instrumenting) を行う必要があります。
計装を行うことで Traces などの telemetry data を生成して、それをOTLP (OpenTelemetry Protocol)を利用して Collector へ送信します。

計装の方法には下記の 2 種類存在します。

  • Automatic Instrumentation: 言語やフレームワークごとに telemetry data を送信する処理を実装した package が用意されており、それを利用して Span の生成などのコードを書かずに計装を行うことができる。
  • Manual Instrumentation: OpenTelemetry SDK を利用して、Span の生成などの計装を行う。

signals (traces)

https://opentelemetry.io/docs/concepts/signals/traces

telemetry data の中でも Trace に絞って検証するので、上記のドキュメントも確認します。
計装に関連するコンポーネントに関する説明もあったので、下記にまとめます。

  • Tracer Provider: Tracer の Factoryで、アプリケーションのライフサイクルと同じタイミング (e.g. アプリケーションの起動時) で一度だけ初期化される
  • Tracer: サービス内のリクエストなどの処理で何が起こっているかについての詳細情報を含んだSpan を作成する
    • 詳細情報の中には、Trace / Span の関係性に関するメタデータとしての Trace Context も含まれる
  • Trace Exporter: OpenTelemetry Collector などの consumer にTrace を送信する

collector

https://opentelemetry.io/docs/concepts/data-collection/

アプリケーションから送信された Trace などの telemetry data については、collector を利用してデータの受信、処理、エクスポートを行います。
下記にも記載のある通り、提供している機能ごとにコンポーネントが分かれています。

The Collector consists of three components that access telemetry data:

  • Receivers : データの受信
    • pull / pushは選択可能でReceiverの実装に依存する
  • Processors : データの処理
  • Exporters : (backendへの) データのエキスポート

コンポーネントごとにパッケージとして実装されることもあり、今回利用する Google Cloud Trace に Trace を送信する Exporter に関しては下記から参照できます。

https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/tree/main/opentelemetry-exporter-gcp-trace

collector に関する、より詳細な設計は下記にまとめられています。

https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/design.md

アプリケーションでの計装

全体像をざっと押さえたところで、アプリケーションの実装に入ります。
今回は、Fast API ベースのアプリケーションで出力した Trace を Google Cloud Trace に送信するというものです。

アプリケーションは Cloud run にデプロイできるように実装しています。

https://github.com/taxintt/fastapi-with-otel

Automatic instrumentation (opentelemetry-instrumentation-fastapi)

まずは、signals (traces)のセクションでも説明した通り、Tracer Provider の初期化を行います。

tracer_provider = TracerProvider()
trace.set_tracer_provider(tracer_provider=tracer_provider)

Automatic Instrumentationに関しては、今回の記事ではopentelemetry-instrumentation-fastapiを利用します。

instrumentation/fastapi/__init__.pyをみると、サンプルコードがあるのでそれを参考にします。

instrumentor = otel_fastapi.FastAPIInstrumentor()
instrumentor.instrument_app(
    app=app,
    server_request_hook=_server_request_hook,
    client_request_hook=_client_request_hook,
    client_response_hook=_client_response_hook,
    tracer_provider=tracer_provider
)

記載されている説明を見ていると、Request/Response に対する hooks を仕込むことができるようです。

This instrumentation supports request and response hooks.
These are functions that get called right after a span is created for a request and right before the span is finished for the response.

  • The server request hook is passed a server span and ASGI scope object for every incoming request.
  • The client request hook is called with the internal span and an ASGI scope when the method receive is called.
  • The client response hook is called with the internal span and an ASGI event when the method send is called.

hook のタイミングは記載のある通りですが、どのような Span が作成されるか気になるのでテストコードを参考に hook を仕込んでみます。

https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py

def _server_request_hook(span, scope):
    span.update_name("name from server hook")


def _client_request_hook(receive_span, request):
    receive_span.update_name("name from client hook")
    receive_span.set_attribute("attr-from-request-hook", "set")


def _client_response_hook(send_span, response):
    send_span.update_name("name from response hook")
    send_span.set_attribute("attr-from-response-hook", "value")

Manual Instrumentation

Manual Instrumentationでは、opentelemetry-pythonを参考に SDK を利用して Trace の生成を試します。

docs/examples以下には実装サンプルがあり、それを参考にしました。
https://github.com/open-telemetry/opentelemetry-python/blob/main/docs/examples/basic_tracer/basic_trace.py

/sampleのエンドポイントに対して、get_sampleという名前で Span を生成しています。
Span 同士を適切に関連づける (Context Propagation) ために、忘れずに tracer を引きまわすようにしましょう。

@sample_router.get("/sample")
async def get_sample():
    tracer = trace.get_tracer_provider().get_tracer(__name__)
    with tracer.start_as_current_span("get_sample"):
        time.sleep(2)
        return {"sample_id": random.randint(1, 10)}

ここでは、個別にスパンを生成する関数 (start_as_current_span()) を利用していますが、デコレーターを利用することで計装を省力化する方法もありそうです。

https://caddi.tech/archives/3312#OpenTelemetry

Cloud TraceへのTraceの送信

計装が一通りできたので、今度は collector 側を調整します。
Google Cloud Trace へ Trace データを送信するので、先述の通り opentelemetry-exporter-gcp-trace を利用します。

ローカルでの実行時には標準出力に吐かせたいので、ConsoleSpanExporterを利用します。
(exporter の環境ごとの有効化は下記のように実装しています。)

tracer_provider = trace.get_tracer_provider()
if STAGE == "local":
    tracer_provider.add_span_processor(
        span_processor=SimpleSpanProcessor(span_exporter=ConsoleSpanExporter()),
    )
else:
    # https://cloud.google.com/trace/docs/setup/python-ot?hl=ja#import
    tracer_provider.add_span_processor(
        span_processor=SimpleSpanProcessor(
            span_exporter=CloudTraceSpanExporter(
                project_id=os.environ.get("GCP_PROJECT"),
            ),
        ),
    )

一点注意点として、このアプリケーションを Cloud run で動かす場合には、SimpleSpanProcessor を利用する必要があります。

フォアグラウンド プロセスを使用してスパンを送信するには、SimpleSpanProcessor プロセッサを使用します。Cloud Run を使用している場合は、このプロセッサを使用する必要があります。このプロセッサでは、アプリケーションの動作が遅くなることがあります。

Cloud Traceへ送信されたTraceデータの確認

実際に送信された Trace のデータを見てみましょう。

trace_screenshot

トレースの詳細を見ると、下記のことがわかります。

  • HTTP Request (GET /sample) に対して、実際の処理 (get_sample) がどの程度かかったか
  • Cloud run でのアプリケーションの起動にどの程度時間がかかったのか
    • server hook の開始時間を確認すると、ざっと 4 秒ほどかかったと推測される

最後に

ざっくりではありますが、OpenTelemetry の概要と Fast API ベースのアプリケーションを利用した技術検証の結果についてまとめました。

他の方のアドベントカレンダーの記事や参考事例にもある通り、サンプリングレートなど導入にあたっての考慮事項は色々あります。

また、自動計装の実装として用意されている機能が (紹介していないですが) 他にもあるので、その辺りの棲み分けを考える必要もありそうです。
https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi

それらの考慮事項を理解するための、前提となるベースの知識を身につける際に有用な記事になっていれば嬉しいです。

Discussion