CLOVER🍀

That was when it all began.

PythonのOpenTelemetry SDKを使って、メトリクスを送信する単純なスクリプトを作成する

これは、なにをしたくて書いたもの?

OpenTelemetryを使っている時に、ちょっとした動作確認みたいなことをしたい時があったりします。

この時に、アプリケーションを作って計装ライブラリーをインストールして…みたいなことをしていると手間な気がするので、簡単なスクリプト
作ってこれができるようにしてみようかなと。

OpenTelemetry API、OpenTelemetry SDKの勉強がてらに。言語はPythonを使います。

OpenTelemetryの言語APISDK

OpenTelemetryの各言語向けのAPISDKに関するページはこちら。

Language APIs & SDKs | OpenTelemetry

Pythonに関するページはこちら。

Python | OpenTelemetry

通常はなんらかのフレームワークやライブラリーを使って開発したアプリケーションにOpenTelemetryを組み込むため、Getting Startedには
ゼロコード計装の例が紹介されています。

Getting Started | OpenTelemetry

今回参照するのはこちらですね。

Instrumentation | OpenTelemetry

アプリケーションに計装を組み込むためには、OpenTelemetry SDKを使います。各シグナルを操作する時にはOpenTelemetry APIを使うことに
なります。実際にシグナルを送信するためにはOpenTelemetry SDKが必要です。

計装に関しては、こちらも見るとよいでしょう。

Instrumentation | OpenTelemetry

ゼロコード計装、コードベース、ライブラリーが載っていますが、今回はコードベースの計装を行うことになります。

Code-based | OpenTelemetry

PythonのOpenTelemetry APIおよびSDKに関するリファレンスはこちら。

OpenTelemetry-Python API Reference — OpenTelemetry Python documentation

OpenTelemetry Python API — OpenTelemetry Python documentation

OpenTelemetry Python SDK — OpenTelemetry Python documentation

今回はメトリクスを扱ってみます。

Instrumentation / Metrics

送信先(Exporter)はコンソール(標準出力)とOTLPの2つにします。

最後に設定の自動構成までやってみましょう。

環境

今回の環境はこちら。

$ python3 --version
Python 3.12.3


$ uv --version
uv 0.8.15

OpenTelemetry Collector Contribはこちら。

$ otelcol-contrib --version
otelcol-contrib version 0.134.0

OpenTelemetry Collector ContribのIPアドレスは172.17.0.2とします。

設定はデフォルトのままです。

/etc/otelcol-contrib/config.yaml

# To limit exposure to denial of service attacks, change the host in endpoints below from 0.0.0.0 to a specific network interface.
# See https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/security-best-practices.md#safeguards-against-denial-of-service-attacks

extensions:
  health_check:
  pprof:
    endpoint: 0.0.0.0:1777
  zpages:
    endpoint: 0.0.0.0:55679

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

  # Collect own metrics
  prometheus:
    config:
      scrape_configs:
      - job_name: 'otel-collector'
        scrape_interval: 10s
        static_configs:
        - targets: ['0.0.0.0:8888']

  jaeger:
    protocols:
      grpc:
        endpoint: 0.0.0.0:14250
      thrift_binary:
        endpoint: 0.0.0.0:6832
      thrift_compact:
        endpoint: 0.0.0.0:6831
      thrift_http:
        endpoint: 0.0.0.0:14268

  zipkin:
    endpoint: 0.0.0.0:9411

processors:
  batch:

exporters:
  debug:
    verbosity: detailed

service:

  pipelines:

    traces:
      receivers: [otlp, jaeger, zipkin]
      processors: [batch]
      exporters: [debug]

    metrics:
      receivers: [otlp, prometheus]
      processors: [batch]
      exporters: [debug]

    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]

  extensions: [health_check, pprof, zpages]

準備

uvでプロジェクトを作成します。

$ uv init --vcs none simple-metrics-sender
$ cd simple-metrics-sender
$ rm main.py

必要なライブラリーをインストール。この後でまた追加します。

$ uv add opentelemetry-api opentelemetry-sdk
$ uv add --dev mypy ruff

pyproject.toml

[project]
name = "simple-metrics-sender"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "opentelemetry-api>=1.36.0",
    "opentelemetry-sdk>=1.36.0",
]

[dependency-groups]
dev = [
    "mypy>=1.17.1",
    "ruff>=0.12.12",
]

[tool.mypy]
strict = true
disallow_any_unimported = true
disallow_any_expr = true
disallow_any_explicit = true
warn_unreachable = true
pretty = true

インストールした依存ライブラリーの一覧。

$ uv pip list
Package                            Version
---------------------------------- -------
importlib-metadata                 8.7.0
mypy                               1.17.1
mypy-extensions                    1.1.0
opentelemetry-api                  1.36.0
opentelemetry-sdk                  1.36.0
opentelemetry-semantic-conventions 0.57b0
pathspec                           0.12.1
ruff                               0.12.12
typing-extensions                  4.15.0
zipp                               3.23.0

ここからスタートです。

メトリクスを標準出力に書き出す

最初はメトリクスを標準出力に書き出してみましょう。

このあたりを見ながら

Instrumentation / Metrics

ソースコードを作成。

app.py

from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import (
    ConsoleMetricExporter,
    PeriodicExportingMetricReader,
)

metric_reader = PeriodicExportingMetricReader(ConsoleMetricExporter())
provider = MeterProvider(metric_readers=[metric_reader])

metrics.set_meter_provider(provider)

meter = metrics.get_meter("my.app.telemetry")

gause = meter.create_gauge("my.gause", unit="value", description="Sample Gause")
gause.set(15)

counter = counter = meter.create_counter(
    "my.counter", unit="count", description="Sample Counter"
)
counter.add(5)

provider.shutdown()

今回はメトリクスのうちGaugeとCounterを使うことにしました。

Metrics / Metric Instruments

service.name環境変数で設定しました。

$ export OTEL_SERVICE_NAME=app

実行。すぐに終了して、標準出力にメトリクスが書き出されます。

$ uv run app.py
{
    "resource_metrics": [
        {
            "resource": {
                "attributes": {
                    "telemetry.sdk.language": "python",
                    "telemetry.sdk.name": "opentelemetry",
                    "telemetry.sdk.version": "1.36.0",
                    "service.name": "app"
                },
                "schema_url": ""
            },
            "scope_metrics": [
                {
                    "scope": {
                        "name": "my.app.telemetry",
                        "version": "",
                        "schema_url": "",
                        "attributes": null
                    },
                    "metrics": [
                        {
                            "name": "my.gause",
                            "description": "Sample Gause",
                            "unit": "value",
                            "data": {
                                "data_points": [
                                    {
                                        "attributes": {},
                                        "start_time_unix_nano": null,
                                        "time_unix_nano": 1757169750858828777,
                                        "value": 15,
                                        "exemplars": []
                                    }
                                ]
                            }
                        },
                        {
                            "name": "my.counter",
                            "description": "Sample Counter",
                            "unit": "count",
                            "data": {
                                "data_points": [
                                    {
                                        "attributes": {},
                                        "start_time_unix_nano": 1757169750858751308,
                                        "time_unix_nano": 1757169750858828777,
                                        "value": 5,
                                        "exemplars": []
                                    }
                                ],
                                "aggregation_temporality": 2,
                                "is_monotonic": true
                            }
                        }
                    ],
                    "schema_url": ""
                }
            ],
            "schema_url": ""
        }
    ]
}

OKですね。

OTLPで送信する

次はOTLPで送信してみましょう。

opentelemetry-exporter-otlpを追加。

$ uv add opentelemetry-exporter-otlp

pyproject.tomldependenciesはこうなりました。

dependencies = [
    "opentelemetry-api>=1.36.0",
    "opentelemetry-exporter-otlp>=1.36.0",
    "opentelemetry-sdk>=1.36.0",
]

インストールしたライブラリーの一覧。

$ uv pip list
Package                                  Version
---------------------------------------- --------
certifi                                  2025.8.3
charset-normalizer                       3.4.3
googleapis-common-protos                 1.70.0
grpcio                                   1.74.0
idna                                     3.10
importlib-metadata                       8.7.0
mypy                                     1.17.1
mypy-extensions                          1.1.0
opentelemetry-api                        1.36.0
opentelemetry-exporter-otlp              1.36.0
opentelemetry-exporter-otlp-proto-common 1.36.0
opentelemetry-exporter-otlp-proto-grpc   1.36.0
opentelemetry-exporter-otlp-proto-http   1.36.0
opentelemetry-proto                      1.36.0
opentelemetry-sdk                        1.36.0
opentelemetry-semantic-conventions       0.57b0
pathspec                                 0.12.1
protobuf                                 6.32.0
requests                                 2.32.5
ruff                                     0.12.12
typing-extensions                        4.15.0
urllib3                                  2.5.0
zipp                                     3.23.0

ソースコードはこのようなものを作成。

app_otlp.py

from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter

metric_reader = PeriodicExportingMetricReader(
    OTLPMetricExporter(endpoint="http://172.17.0.2:4318/v1/metrics")
)
provider = MeterProvider(metric_readers=[metric_reader])

metrics.set_meter_provider(provider)

meter = metrics.get_meter("my.app.telemetry")

gause = meter.create_gauge("my.gause", unit="value", description="Sample Gause")
gause.set(15)

counter = counter = meter.create_counter(
    "my.counter", unit="count", description="Sample Counter"
)
counter.add(5)

provider.shutdown()

先ほどとの違いはExporterですね。

metric_reader = PeriodicExportingMetricReader(
    OTLPMetricExporter(endpoint="http://172.17.0.2:4318/v1/metrics")
)

実行。

$ uv run app_otlp.py

今回はOpenTelemetry Collector Contrib側でこんな出力が得られます。

2025-09-06T15:05:46.781Z        info    Metrics {"resource": {"service.instance.id": "822bf5e0-e250-48f4-89fb-2df940d8f950", "service.name": "otelcol-contrib", "service.version": "0.134.0"}, "otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "metrics", "resource metrics": 1, "metrics": 2, "data points": 2}
2025-09-06T15:05:46.781Z        info    ResourceMetrics #0
Resource SchemaURL:
Resource attributes:
     -> telemetry.sdk.language: Str(python)
     -> telemetry.sdk.name: Str(opentelemetry)
     -> telemetry.sdk.version: Str(1.36.0)
     -> service.name: Str(app)
ScopeMetrics #0
ScopeMetrics SchemaURL:
InstrumentationScope my.app.telemetry
Metric #0
Descriptor:
     -> Name: my.gause
     -> Description: Sample Gause
     -> Unit: value
     -> DataType: Gauge
NumberDataPoints #0
StartTimestamp: 1970-01-01 00:00:00 +0000 UTC
Timestamp: 2025-09-06 15:05:46.737547332 +0000 UTC
Value: 15
Metric #1
Descriptor:
     -> Name: my.counter
     -> Description: Sample Counter
     -> Unit: count
     -> DataType: Sum
     -> IsMonotonic: true
     -> AggregationTemporality: Cumulative
NumberDataPoints #0
StartTimestamp: 2025-09-06 15:05:46.73741025 +0000 UTC
Timestamp: 2025-09-06 15:05:46.737547332 +0000 UTC
Value: 5
        {"resource": {"service.instance.id": "822bf5e0-e250-48f4-89fb-2df940d8f950", "service.name": "otelcol-contrib", "service.version": "0.134.0"}, "otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "metrics"}

これはOpenTelemetry Collector Contribでの出力設定がdebugだからですね。

    metrics:
      receivers: [otlp, prometheus]
      processors: [batch]
      exporters: [debug]

こちらもOKでした。

自動構成する

ここまで、Exporterを固定のクラス名で書いてきました。

実際に使う時は、環境変数で切り替えたいですね。

ここで、OpenTelemetry Distroを使います。

OpenTelemetry Distro | OpenTelemetry

正確には、opentelemetry-instrumentationに環境変数から設定を読み取って自動構成する処理が含まれているんですよね。

opentelemetry-python-contrib/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py at v0.57b0 · open-telemetry/opentelemetry-python-contrib · GitHub

依存ライブラリーを追加。

$ uv add opentelemetry-distro[otlp] opentelemetry-instrumentation
$ uv add --dev mypy ruff

pyproject.tomldependenciesは作り直しました。

dependencies = [
    "opentelemetry-distro[otlp]>=0.57b0",
    "opentelemetry-instrumentation>=0.57b0",
]

[dependency-groups]
dev = [
    "mypy>=1.17.1",
    "ruff>=0.12.12",
]

インストールしたライブラリーの一覧。

$ uv pip list
Package                                  Version
---------------------------------------- --------
certifi                                  2025.8.3
charset-normalizer                       3.4.3
googleapis-common-protos                 1.70.0
grpcio                                   1.74.0
idna                                     3.10
importlib-metadata                       8.7.0
mypy                                     1.17.1
mypy-extensions                          1.1.0
opentelemetry-api                        1.36.0
opentelemetry-distro                     0.57b0
opentelemetry-exporter-otlp              1.36.0
opentelemetry-exporter-otlp-proto-common 1.36.0
opentelemetry-exporter-otlp-proto-grpc   1.36.0
opentelemetry-exporter-otlp-proto-http   1.36.0
opentelemetry-instrumentation            0.57b0
opentelemetry-proto                      1.36.0
opentelemetry-sdk                        1.36.0
opentelemetry-semantic-conventions       0.57b0
packaging                                25.0
pathspec                                 0.12.1
protobuf                                 6.32.0
requests                                 2.32.5
ruff                                     0.12.12
typing-extensions                        4.15.0
urllib3                                  2.5.0
wrapt                                    1.17.3
zipp                                     3.23.0

ソースコードはこのように変更。

app_auto.py

from opentelemetry import metrics

provider = metrics.get_meter_provider()

meter = metrics.get_meter("my.app.telemetry")

gause = meter.create_gauge("my.gause", unit="value", description="Sample Gause")
gause.set(15)

counter = counter = meter.create_counter(
    "my.counter", unit="count", description="Sample Counter"
)
counter.add(5)

provider.shutdown()

Exporterのクラスが固定されなくなりました。

設定を環境変数で行います。

$ export OTEL_TRACES_EXPORTER=none
$ export OTEL_METRICS_EXPORTER=otlp
$ export OTEL_LOGS_EXPORTER=none
$ export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://172.17.0.2:4318/v1/metrics
$ export OTEL_EXPORTER_OTLP_METRICS_PROTOCOL=http/protobuf
$ export OTEL_SERVICE_NAME=app

実行。

$ uv run opentelemetry-instrument python3 app_auto.py

OpenTelemetry Collector Contrib側にはこんな結果が得られます。先ほどと同じですね。

2025-09-06T15:33:50.904Z        info    Metrics {"resource": {"service.instance.id": "ec9f7f2a-240f-4ffa-9cf0-70dc78965996", "service.name": "otelcol-contrib", "service.version": "0.134.0"}, "otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "metrics", "resource metrics": 1, "metrics": 2, "data points": 2}
2025-09-06T15:33:50.905Z        info    ResourceMetrics #0
Resource SchemaURL:
Resource attributes:
     -> telemetry.sdk.language: Str(python)
     -> telemetry.sdk.name: Str(opentelemetry)
     -> telemetry.sdk.version: Str(1.36.0)
     -> service.name: Str(app)
     -> telemetry.auto.version: Str(0.57b0)
ScopeMetrics #0
ScopeMetrics SchemaURL:
InstrumentationScope my.app.telemetry
Metric #0
Descriptor:
     -> Name: my.gause
     -> Description: Sample Gause
     -> Unit: value
     -> DataType: Gauge
NumberDataPoints #0
StartTimestamp: 1970-01-01 00:00:00 +0000 UTC
Timestamp: 2025-09-06 15:33:50.806934958 +0000 UTC
Value: 15
Metric #1
Descriptor:
     -> Name: my.counter
     -> Description: Sample Counter
     -> Unit: count
     -> DataType: Sum
     -> IsMonotonic: true
     -> AggregationTemporality: Cumulative
NumberDataPoints #0
StartTimestamp: 2025-09-06 15:33:50.806893705 +0000 UTC
Timestamp: 2025-09-06 15:33:50.806934958 +0000 UTC
Value: 5
        {"resource": {"service.instance.id": "ec9f7f2a-240f-4ffa-9cf0-70dc78965996", "service.name": "otelcol-contrib", "service.version": "0.134.0"}, "otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "metrics"}

ここで出力先を標準出力にしてみましょう。

$ export OTEL_METRICS_EXPORTER=console

今度はメトリクスが標準出力に書き出されるようになりました。

$ uv run opentelemetry-instrument python3 app_auto.py
{
    "resource_metrics": [
        {
            "resource": {
                "attributes": {
                    "telemetry.sdk.language": "python",
                    "telemetry.sdk.name": "opentelemetry",
                    "telemetry.sdk.version": "1.36.0",
                    "service.name": "app",
                    "telemetry.auto.version": "0.57b0"
                },
                "schema_url": ""
            },
            "scope_metrics": [
                {
                    "scope": {
                        "name": "my.app.telemetry",
                        "version": "",
                        "schema_url": "",
                        "attributes": null
                    },
                    "metrics": [
                        {
                            "name": "my.gause",
                            "description": "Sample Gause",
                            "unit": "value",
                            "data": {
                                "data_points": [
                                    {
                                        "attributes": {},
                                        "start_time_unix_nano": null,
                                        "time_unix_nano": 1757172889317428348,
                                        "value": 15,
                                        "exemplars": []
                                    }
                                ]
                            }
                        },
                        {
                            "name": "my.counter",
                            "description": "Sample Counter",
                            "unit": "count",
                            "data": {
                                "data_points": [
                                    {
                                        "attributes": {},
                                        "start_time_unix_nano": 1757172889317347929,
                                        "time_unix_nano": 1757172889317428348,
                                        "value": 5,
                                        "exemplars": []
                                    }
                                ],
                                "aggregation_temporality": 2,
                                "is_monotonic": true
                            }
                        }
                    ],
                    "schema_url": ""
                }
            ],
            "schema_url": ""
        }
    ]
}

OKですね。

おわりに

PythonのOpenTelemetry SDKを使って、メトリクスを送信する単純なスクリプトを作成してみました。

Exporterを決め打ちするのならサンプルはあるのですが、opentelemetry-instrumentationを単体で使った例はなかったのでちょっと苦労しました。
まあ、前にOpenTelemetry Distroやその周辺を見ておいてよかったです。

PythonのOpenTelemetry計装ライブラリーの導入方法がよくわからなかったので、pipとuvを使ってFastAPIで試してみる - CLOVER🍀