ラベル OpenTelemetry の投稿を表示しています。 すべての投稿を表示
ラベル OpenTelemetry の投稿を表示しています。 すべての投稿を表示

2025年3月13日木曜日

OpenTelemetry で celery の非同期タスクでもリクエストと同じトレースをする方法

OpenTelemetry で celery の非同期タスクでもリクエストと同じトレースをする方法

概要

Flask アプリから celery の非同期タスクを呼び出した際にもトレースできるようにします
Flask、Celery で一貫して OLTP にトレース情報を送信するサンプルコードを紹介します

環境

  • macOS 15.3.1
  • Python 3.12.9
    • flask 3.1.0
    • open-telemetry 0.51b0
    • celery 5.4.0
  • OpenTelemetry Collector 0.121.0
  • Jaeger (all in one) 1.67.0

追加インストール

前回のに追加で以下をインストールします

  • pipenv install opentelemetry-instrumentation-celery celery

app.py

from celery import Celery
from flask import Flask
from opentelemetry import trace
from opentelemetry.propagate import inject

from lib.tasks import roll_dice_async

# Flask アプリの作成
app = Flask(__name__)

# Celery 設定
app.config["CELERY_BROKER_URL"] = "redis://localhost:6379/0"
app.config["CELERY_RESULT_BACKEND"] = "redis://localhost:6379/0"
celery = Celery(app.name, broker=app.config["CELERY_BROKER_URL"])
celery.conf.update(app.config)

# OpenTelemetry の設定
tracer = trace.get_tracer("diceroller.tracer")


@app.route("/rolldice")
def roll_dice():
    username = get_username()

    # トレースコンテキストを取得して Celery に渡す
    headers = {}
    inject(headers)
    roll_dice_async.apply_async(kwargs={"username": username, "headers": headers})

    return "Rolling dice asynchronously!"


def get_username():
    with tracer.start_as_current_span("get_username") as span:
        username = "hawksnowlog"
        span.set_attribute("username", username)
        return username


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

lib/tasks.py

from random import randint

from opentelemetry import trace
from opentelemetry.propagate import extract

from lib.worker import celery

tracer = trace.get_tracer("diceroller.tracer")


@celery.task
def roll_dice_async(username, headers):
    return roll(username, headers)


def roll(username: str, headers):
    # OpenTelemetry のコンテキストを受け取り、トレースを継続
    with tracer.start_as_current_span("roll", context=extract(headers)) as span:
        res = randint(1, 6)
        span.set_attribute("roll.value", res)
        return res

lib/worker.py

from celery import Celery
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.celery import CeleryInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

# Celery インスタンスを作成
celery = Celery("tasks", broker="redis://localhost:6379/0")
celery.conf.update(broker_connection_retry_on_startup=True)

# OpenTelemetry の設定
tracer_provider = TracerProvider()
tracer_provider.add_span_processor(
    BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317"))
)

# Celery の OpenTelemetry を有効化
CeleryInstrumentor().instrument()

起動

Flask

  • export OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true && pipenv run opentelemetry-instrument --logs_exporter otlp --service_name dice-server python app.py

Celery

  • export OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true && pipenv run opentelemetry-instrument --logs_exporter otlp --service_name dice-server celery -A lib.tasks worker --loglevel=INFO

Jaeger

  • docker run --rm --name jaeger -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 -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

動作確認

localhost:8080/rolldice にアクセスして Jaeger にアクセスすると一つのトレース内に Flask と Celery のタスクが処理が含まれていることが確認できると思います

最後に

Flask + Celery に OLTP のトレースを設定する方法を紹介しました

トレースに間があるのは非同期特有なのかもしれません
もしくは BatchSpanProcessor を使っているのでリアルタイムでなくバッチ処理として送信している影響かもしれません

参考サイト

2025年3月12日水曜日

OpenTelemetry のトレース情報を Jaeger に直接送信する方法

OpenTelemetry のトレース情報を Jaeger に直接送信する方法

概要

これまではアプリ -> コレクタ -> exporter -> Jaeger という経路でトレース情報を送信していました
推奨ではないですがコレクタと exporter をすっ飛ばして直接トレース情報を Jaeger に送信することもできるので紹介します

環境

  • macOS 15.3.1
  • Python 3.12.9
    • flask 3.1.0
    • open-telemetry 0.51b0
  • OpenTelemetry Collector 0.121.0
  • Jaeger (all in one) 1.67.0

Jaeger 起動

  • docker run --rm --name jaeger -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 -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

4317 で OTLP のトレース情報を直接受けます

アプリ起動

  • export OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true && pipenv run opentelemetry-instrument --logs_exporter otlp --service_name dice-server python app.py

上記の設定では otlp は localhost:4317 に送信されます

app.py

前回と変わりませんが記載しておきます

from random import randint
from flask import Flask

from opentelemetry import trace

# Acquire a tracer
tracer = trace.get_tracer("diceroller.tracer")

app = Flask(__name__)

@app.route("/rolldice")
def roll_dice():
    username = get_username()
    return str(roll(username))

def get_username():
    with tracer.start_as_current_span("get_username") as rollspan:
        username = "hawksnowlog"
        rollspan.set_attribute("username", username)
        return username

def roll(username: str):
    # This creates a new span that's the child of the current one
    with tracer.start_as_current_span("roll") as rollspan:
        res = randint(1, 6)
        rollspan.set_attribute("roll.value", res)
        return res

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

動作確認

Jaeger を確認するとトレース情報が確認できます

最後に

おそらく OpenTelemetry が推奨する方法ではないですがこの方法でもトレース情報を送信できます
アプリのメトリックスを取得しない(トレースだけ)かつ OTLP の情報を Jaeger 以外に送らないのであればこの構成でもいいのかもしれません

参考サイト

2025年3月11日火曜日

OpenTelemetry のトレース箇所を独自で設定する方法

OpenTelemetry のトレース箇所を独自で設定する方法

概要

前回 OpenTelemetry を Flask アプリに適用する方法を紹介しました
前回の方法はトレースやメトリックス情報を自動で送信してくれる設定です
今回は手動でトレース情報を送信する方法を紹介します

環境

  • macOS 15.3.1
  • Python 3.12.9
    • flask 3.1.0
    • open-telemetry 0.51b0
  • OpenTelemetry Collector 0.121.0
  • Jaeger (all in one) 1.67.0

app.py

自動でトレースする設定は外し自分でトレースしたい箇所に設定します
基本的には tracer.start_as_current_span でトレースしたい箇所にスパンを作成するだけで OK です

以下の設定では2箇所でスパンを作成しています

from random import randint
from flask import Flask

from opentelemetry import trace

# Acquire a tracer
tracer = trace.get_tracer("diceroller.tracer")

app = Flask(__name__)

@app.route("/rolldice")
def roll_dice():
    username = get_username()
    return str(roll(username))

def get_username():
    with tracer.start_as_current_span("get_username") as rollspan:
        username = "hawksnowlog"
        rollspan.set_attribute("username", username)
        return username

def roll(username: str):
    # This creates a new span that's the child of the current one
    with tracer.start_as_current_span("roll") as rollspan:
        res = randint(1, 6)
        rollspan.set_attribute("roll.value", res)
        return res

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

各種起動

アプリ、Jaeger、コレクターを起動します

  • export OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true && pipenv run opentelemetry-instrument --logs_exporter otlp --service_name dice-server python app.py
  • docker run --rm --name jaeger -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 -p 16686:16686 -p 14317:4317 -p 14318:4318 -p 14250:14250 -p 14268:14268 -p 14269:14269 -p 9411:9411 jaegertracing/all-in-one:latest
  • docker run -p 4317:4317 -v $(pwd)/otel-collector-config.yaml:/etc/otel-collector-config.yaml otel/opentelemetry-collector:latest --config=/etc/otel-collector-config.yaml

動作確認

localhost:8080/rolldice にアクセスし Jaeger で確認しましょう
以下のように1つのリクエストに対して2つのスパンが作成されていれば OK です

最後に

OpenTelemetry を使って独自のトレース情報を設定する方法を紹介しました
自動の場合はスパンなどが細かく設定できないのでプロダクションでは独自設定を使うことになると思います

参考サイト

2025年3月10日月曜日

OpenTelemetry を使って flask アプリのトレースをしてみる

OpenTelemetry を使って flask アプリのトレースをしてみる

概要

アプリのリクエストがどこ、どのくらいの時間をかけてを通ったかなど可視化することができます

環境

  • macOS 15.3.1
  • Python 3.12.9
    • flask 3.1.0
    • open-telemetry 0.51b0
  • OpenTelemetry Collector 0.121.0
  • Jaeger (all in one) 1.67.0

各種インストール

  • pipenv install flask
  • pipenv install open-telemetry

app.py

from random import randint
from flask import Flask, request
import logging

app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


@app.route("/rolldice")
def roll_dice():
    player = request.args.get('player', default=None, type=str)
    result = str(roll())
    if player:
        logger.warning("%s is rolling the dice: %s", player, result)
    else:
        logger.warning("Anonymous player is rolling the dice: %s", result)
    return result


def roll():
    return randint(1, 6)


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

OpenTelemetry 用のログを自動で出力するように設定する

作成したアプリに自動でログを出力してくれるようにします

  • pipenv run opentelemetry-bootstrap -a install

これで再度アプリを起動します
OpenTelemetry 用のログが自動で出力されるようになります

  • export OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true && pipenv run opentelemetry-instrument --traces_exporter console --metrics_exporter console --logs_exporter console --service_name dice-server python app.py

これで起動し localhost:8080/rolldice にアクセスするとログが OpenTelemetry 形式になっているが確認できると思いますの

今回はすべてのログを自動で OpenTelemetry 形式にしているので flask の起動ログも OpenTelemetry 形式になっています

コレクターの起動

標準出力に吐かれているログをコレクターに送ることで可視化できます
まずはコレクターを起動します
コレクターは docker イメージが用意されているのでそれを使います

otel-collector-config.yaml

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
exporters:
  # NOTE: Prior to v0.86.0 use `logging` instead of `debug`.
  debug:
    verbosity: detailed
processors:
  batch:
service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [debug]
      processors: [batch]
    metrics:
      receivers: [otlp]
      exporters: [debug]
      processors: [batch]
    logs:
      receivers: [otlp]
      exporters: [debug]
      processors: [batch]

コンテナ起動

  • docker run -p 4317:4317 -v $(pwd)/otel-collector-config.yaml:/etc/otel-collector-config.yaml otel/opentelemetry-collector:latest --config=/etc/otel-collector-config.yaml

これで localhost:4317 にアクセスするとコレクタの画面が確認できます

ログをコレクターに送る

あとは先程標準出力に吐かれていた OpenTelemetry のログを起動したコレクターのエンドポイントに向けます

コレクターに送るためのプラグインが必要になるのでインストールします

  • pipenv install opentelemetry-exporter-otlp

そして再度アプリを OpenTelemetry ログ付きで起動します
console 時にくらべコマンドは短くなっています

  • export OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true && pipenv run opentelemetry-instrument --logs_exporter otlp --service_name dice-server python app.py

すると今度はアプリ側ではなくコレクター側に OpenTelemetry の情報が表示されるのが確認できると思います

トレース情報を Jaeger で可視化する

OpenTelemetry Collector は集めたデータを更に別の場所にエクスポートすることができます
トレースデータを Jaeger にエクスポートして WebUI で確認できるようにしてみます

まずは Jaeger を起動します

  • docker run --rm --name jaeger -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 -p 16686:16686 -p 14317:4317 -p 14318:4318 -p 14250:14250 -p 14268:14268 -p 14269:14269 -p 9411:9411 jaegertracing/all-in-one:latest

いろいろポートを開いていますが今回必要なのは OTLP 用の 4317 (ホスト側は 14317) と WebUI 用の 16686 になります
4317 と 4318 はすでにコレクターで使っているので同一ホストで動作させる場合は別ポートをしていします

次に OpenTelemetry Collector の設定ファイルを編集して exporters を追加します

  • vim otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
exporters:
  debug:
    verbosity: detailed
  otlp/jaeger:
    endpoint: 192.168.1.88:14317
    tls:
      insecure: true
processors:
  batch:
service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlp/jaeger]
      processors: [batch]
    metrics:
      receivers: [otlp]
      exporters: [debug]
      processors: [batch]
    logs:
      receivers: [otlp]
      exporters: [debug]
      processors: [batch]

追加したのは exporters の部分です
ここに起動した Jaeger の OTLP ポートを指定しています
コレクターはコンテナで動いているので localhost ではなく Jaeger コンテナが動作しているホストの IP を指定しましょう
そして service.pipelines.traces.exporters を debug から otlp/jaeger に変更します

そしてコレクターを再起動しましょう

動作確認

Flask アプリ、OpenTelemetry Collector、Jaeger をそれぞれ起動し動作確認します
すべて起動できたら localhost:8080/rolldice にアクセスしましょう
そしてトレース情報を Jaeger で確認するため localhost:16686 にアクセスしてみましょう

今回追加した dice-server のトレースが見れるようになっていれば OK です

最後に

Flask アプリに OpenTelemetry を導入してトレース情報を Jaeger で可視化してみました
OpenTelemetry でトレースを可視化する流れは理解できたかなと思います

直接 OpenTelemetry 情報を Jaeger に送る方法はどうやら廃止されるらしく今後はコレクターを経由して Jaeger など別の可視化アプリなどに連携するのが主流になるようです

今回はすべてのログに自動で OpenTelemetry 化する方法を採用しましたがプロダクションなどであれば必要な箇所でスパンなどを定義して送信する必要があります
そのあたりの個別のスパンの作成方法なども機会があれば紹介したいと思います

参考サイト