これは、なにをしたくて書いたもの?
Prometheusといえばpull型のメトリクス収集ソフトウェアですが、リモート書き込みの他にOpenTelemetryのメトリクスシグナルを
直接受け取ることもできるようです。
ちょっと試してみましょう。
PrometheusをOpenTelemetryのバックエンドとして使う
ドキュメントとしてはこちらです。
Using Prometheus as your OpenTelemetry backend | Prometheus
デフォルトでは無効になっていますが、起動時に--web.enable-otlp-receiverというフラグを付与することでPrometheusでOpenTelemetryの
メトリクスを受け取ることができます。
デフォルトで無効なのは、Prometheusには認証の仕組みがないからですね。
ポイントはこのあたりでしょうか。
- OpenTelemetryシグナルのうち、受け取れるのはメトリクスのみ
- プロトコルはhttp/protobufで、gRPCは不可
- OpenTelemetry Collectorはバッチ処理を推奨しているため、順不同の取り込みを無効しておくこと
- 必要に応じて属性のプロモーションを検討すること
- メトリクス名の正規化の設定を確認する
後半2つがちょっとよくわかりません。属性のプロモーションについてはこちらに書いてあるのですが、どうもGrafanaで見る時に扱いにくかった
みたいですね。
今回、このあたりも少し試してみましょう。
環境
今回の環境はこちら。
Prometheus。172.19.0.2で動作しているものとします。
$ ./prometheus --version prometheus, version 3.5.0 (branch: HEAD, revision: 8be3a9560fbdd18a94dedec4b747c35178177202) build user: root@4451b64cb451 build date: 20250714-16:15:23 go version: go1.24.5 platform: linux/amd64 tags: netgo,builtinassets
Grafana。172.19.0.3で動作しているものとします。
$ grafana-server --version Version 12.1.1 (commit: df5de8219b41d1e639e003bf5f3a85913761d167, branch: release-12.1.1)
確認用のアプリケーションはPythonで作成します。
$ python3 --version Python 3.12.3 $ uv --version uv 0.8.17
Grafanaのデータソースの設定はTerraformで行うことにします。
$ terraform version Terraform v1.13.2 on linux_amd64
準備
Prometheusを--web.enable-otlp-receiver付きで起動しておきます。
$ ./prometheus --web.enable-otlp-receiver
設定はいろいろ変えていくので、都度載せることにします。
次にメトリクスを送信するアプリケーションを作成します。
Pythonで簡単なFastAPIを使ったアプリケーションを作成することにしましょう。
$ uv init --vcs none metrics-send-prometheus $ cd metrics-send-prometheus $ rm main.py
ライブラリーのインストール。
$ uv add "fastapi[standard]" $ uv add opentelemetry-distro[otlp] $ uv run opentelemetry-bootstrap -a requirements | xargs uv add $ uv add --dev mypy ruff
インストールされたライブラリーの一覧。
$ uv pip list Package Version ----------------------------------------- -------- annotated-types 0.7.0 anyio 4.10.0 asgiref 3.9.1 certifi 2025.8.3 charset-normalizer 3.4.3 click 8.2.1 dnspython 2.8.0 email-validator 2.3.0 fastapi 0.116.1 fastapi-cli 0.0.11 fastapi-cloud-cli 0.1.5 googleapis-common-protos 1.70.0 grpcio 1.74.0 h11 0.16.0 httpcore 1.0.9 httptools 0.6.4 httpx 0.28.1 idna 3.10 importlib-metadata 8.7.0 jinja2 3.1.6 markdown-it-py 4.0.0 markupsafe 3.0.2 mdurl 0.1.2 mypy 1.18.1 mypy-extensions 1.1.0 opentelemetry-api 1.37.0 opentelemetry-distro 0.58b0 opentelemetry-exporter-otlp 1.37.0 opentelemetry-exporter-otlp-proto-common 1.37.0 opentelemetry-exporter-otlp-proto-grpc 1.37.0 opentelemetry-exporter-otlp-proto-http 1.37.0 opentelemetry-instrumentation 0.58b0 opentelemetry-instrumentation-asgi 0.58b0 opentelemetry-instrumentation-asyncio 0.58b0 opentelemetry-instrumentation-click 0.58b0 opentelemetry-instrumentation-dbapi 0.58b0 opentelemetry-instrumentation-fastapi 0.58b0 opentelemetry-instrumentation-grpc 0.58b0 opentelemetry-instrumentation-httpx 0.58b0 opentelemetry-instrumentation-jinja2 0.58b0 opentelemetry-instrumentation-logging 0.58b0 opentelemetry-instrumentation-requests 0.58b0 opentelemetry-instrumentation-sqlite3 0.58b0 opentelemetry-instrumentation-starlette 0.58b0 opentelemetry-instrumentation-threading 0.58b0 opentelemetry-instrumentation-tortoiseorm 0.58b0 opentelemetry-instrumentation-urllib 0.58b0 opentelemetry-instrumentation-urllib3 0.58b0 opentelemetry-instrumentation-wsgi 0.58b0 opentelemetry-proto 1.37.0 opentelemetry-sdk 1.37.0 opentelemetry-semantic-conventions 0.58b0 opentelemetry-util-http 0.58b0 packaging 25.0 pathspec 0.12.1 protobuf 6.32.1 pydantic 2.11.8 pydantic-core 2.33.2 pygments 2.19.2 python-dotenv 1.1.1 python-multipart 0.0.20 pyyaml 6.0.2 requests 2.32.5 rich 14.1.0 rich-toolkit 0.15.1 rignore 0.6.4 ruff 0.13.0 sentry-sdk 2.37.1 shellingham 1.5.4 sniffio 1.3.1 starlette 0.47.3 typer 0.17.4 typing-extensions 4.15.0 typing-inspection 0.4.1 urllib3 2.5.0 uvicorn 0.35.0 uvloop 0.21.0 watchfiles 1.1.0 websockets 15.0.1 wrapt 1.17.3 zipp 3.23.0
pyproject.toml
[project]
name = "metrics-send-prometheus"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi[standard]>=0.116.1",
"opentelemetry-distro[otlp]>=0.58b0",
"opentelemetry-instrumentation-asyncio==0.58b0",
"opentelemetry-instrumentation-click==0.58b0",
"opentelemetry-instrumentation-dbapi==0.58b0",
"opentelemetry-instrumentation-fastapi==0.58b0",
"opentelemetry-instrumentation-grpc==0.58b0",
"opentelemetry-instrumentation-httpx==0.58b0",
"opentelemetry-instrumentation-jinja2==0.58b0",
"opentelemetry-instrumentation-logging==0.58b0",
"opentelemetry-instrumentation-requests==0.58b0",
"opentelemetry-instrumentation-sqlite3==0.58b0",
"opentelemetry-instrumentation-starlette==0.58b0",
"opentelemetry-instrumentation-threading==0.58b0",
"opentelemetry-instrumentation-tortoiseorm==0.58b0",
"opentelemetry-instrumentation-urllib==0.58b0",
"opentelemetry-instrumentation-urllib3==0.58b0",
"opentelemetry-instrumentation-wsgi==0.58b0",
]
[dependency-groups]
dev = [
"mypy>=1.18.1",
"ruff>=0.13.0",
]
[tool.mypy]
strict = true
disallow_any_unimported = true
disallow_any_expr = true
disallow_any_explicit = true
warn_unreachable = true
pretty = true
アプリケーションを作成。
app.py
from fastapi import FastAPI from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor app = FastAPI() @app.get("/hello") def hello() -> dict[str, str]: return {"message": "Hello World"} FastAPIInstrumentor.instrument_app(app)
動作確認しておきます。
$ uv run fastapi run app.py
OKですね。
$ curl localhost:8000/hello
{"message":"Hello World"}
最後はTerraformでGrafanaのリソース定義を行います。
※作業の都合上、ちょいちょい作り直したりするのでこの方が便利なので
terraform.tf
terraform { required_version = "1.13.2" required_providers { grafana = { source = "grafana/grafana" version = "4.7.1" } } }
main.tf
provider "grafana" { url = "http://172.19.0.3:3000" auth = "admin:admin" } resource "grafana_data_source" "prometheus" { name = "prometheus" type = "prometheus" url = "http://172.19.0.2:9090" is_default = true json_data_encoded = jsonencode({ httpMethod = "POST" prometheusType = "Prometheus" }) }
リソース作成。
$ terraform init $ terraform apply
これで準備完了です。
PrometheusにOpenTelemetryのメトリクスシグナルを送信する
それでは、PrometheusにOpenTelemetryのメトリクスシグナルを送信してみましょう。
Prometheusの設定を変えながらいろいろと試してみたいと思います。
OpenTelemetry SDKの設定は以下のようにしておきます。
$ export OTEL_TRACES_EXPORTER=none $ export OTEL_METRICS_EXPORTER=otlp $ export OTEL_LOGS_EXPORTER=none $ export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://172.19.0.2:9090/api/v1/otlp/v1/metrics $ export OTEL_EXPORTER_OTLP_METRICS_PROTOCOL=http/protobuf $ export OTEL_METRIC_EXPORT_INTERVAL=5000 $ export OTEL_SERVICE_NAME=app
OTEL_EXPORTER_OTLP_METRICS_ENDPOINTはPrometheusのエンドポイントです。ポートは通常のPrometheusのものと同じですが、
パスがOpenTelemetryが期待するものとはだいぶ違いますね。 OTEL_EXPORTER_OTLP_METRICS_PROTOCOLはhttp/protobufにしましょう。
OTEL_METRIC_EXPORT_INTERVALは5秒にして、短い感覚でメトリクスを送信します。
起動。
$ uv run opentelemetry-instrument fastapi run app.py
あとは適当にメトリクスを送信し続けます。
$ watch curl -s localhost:8000/hello
ちなみに、Prometheusに--web.enable-otlp-receiverをつけるのを忘れて起動した場合は、FastAPI側に以下のメッセージが表示されます。
Failed to export metrics batch code: 404, reason: otlp write receiver needs to be enabled with --web.enable-otlp-receiver
この時、Prometheusの設定を変えながらGrafanaなどのUI上にどう反映されるかを見てみましょう。
ストレージ設定で、順不同の取り込みを無効にする
まずはPrometheusのストレージ設定で、順不同の取り込みを無効にのみしておきます。
prometheus.yml
global:
scrape_configs:
storage:
tsdb:
out_of_order_time_window: 30m
結果を確認。
Prometheus。

Grafana。

ちゃんと表示されています。つまり、OpenTelemetry SDKで送信したメトリクスをPrometheusで取り込めていることを確認できました。
属性のプロモーションを有効にする
次は属性のプロモーションを有効にしてみましょう。
今回はドキュメントに記載されている値をそのまま貼ってみます。
prometheus.yml
global: scrape_configs: otlp: promote_resource_attributes: - service.instance.id - service.name - service.namespace - service.version - cloud.availability_zone - cloud.region - container.name - deployment.environment - deployment.environment.name - k8s.cluster.name - k8s.container.name - k8s.cronjob.name - k8s.daemonset.name - k8s.deployment.name - k8s.job.name - k8s.namespace.name - k8s.pod.name - k8s.replicaset.name - k8s.statefulset.name storage: tsdb: out_of_order_time_window: 30m
確認。
Prometheus。

Grafana。

さて、なにか変わったんのでしょうか…?
今回の例だととてもわかりにくいのですが、メトリクスのラベルにservice_nameが増えているのが確認できると思います。
Label filtersでも選べるようになっています。

promote_resource_attributesの設定を入れる前は、こうでした。

これが属性のプロモーションということですね。
ちなみにプロモーションしていない属性は、target_infoというメトリクスのラベルになっています。


https://prometheus.io/docs/guides/opentelemetry/#including-resource-attributes-at-query-time
メトリクス名の正規化の設定をする
最後はメトリクス名の正規化の設定です。
prometheus.yml
global: scrape_configs: otlp: promote_resource_attributes: - service.instance.id - service.name - service.namespace - service.version - cloud.availability_zone - cloud.region - container.name - deployment.environment - deployment.environment.name - k8s.cluster.name - k8s.container.name - k8s.cronjob.name - k8s.daemonset.name - k8s.deployment.name - k8s.job.name - k8s.namespace.name - k8s.pod.name - k8s.replicaset.name - k8s.statefulset.name translation_strategy: NoTranslation storage: tsdb: out_of_order_time_window: 30m
ポイントはここですね。
translation_strategy: NoTranslation
これはなにも変換しない設定です。デフォルト値は互換性を重視したUnderscoreEscapingWithSuffixesのようです。
Metric and label naming | Prometheus
この設定を入れると、メトリクスのデリミターが変わります。"."になっていますね。
Prometheusだと、なんとエラーになります…。

Grafanaでは参照できます。ラベルのデリミターも"."になっていますね。

これを見ると""と"."が混ざっているので、本来の使い分けされているところがすべて""にまとめられていたということですね。
つまり、階層表現をしていたようなところが失われていた、と。
各変換設定の意味を見てみましょう。
- UnderscoreEscapingWithSuffixes … デフォルの変換設定で、従来のPrometheusのメトリクス名の互換性のためにエスケープされ、型や単位のsuffixが付与される
- UnderscoreEscapingWithoutSuffixes … UnderscoreEscapingWithSuffixesと同様にメトリクス名をエスケープするが、型や単位のsuffixは付与しない。suffixがないことで衝突が発生する可能性があることに注意すること
- NoUTF8EscapingWithSuffixes … 特殊文字を"_"に変換する機能を無効にし、OpenTelemetryメトリクスフォーマットをネイティブで利用できるようにする。ただし単位や
_totalカウンターのような特殊なsuffixは同じ名前で異なる型や単位のメトリクスの衝突を防ぐために付加される - NoTranslation … すべてのメトリクス名とラベル名の変換をバイパスする。サフィックスがない場合で、同じ名前のメトリクスが複数存在する時に型や単位が異なる時に競合することがある
およそ設定の意味はわかった感じでしょうか。
おわりに
Prometheusで、OpenTelemetryのメトリクスシグナルを直接受け取れることを確認してみました。
またドキュメントに書かれている設定の意味も確認できました。最初はよくわからなかったのですが、こういうのはやっぱり動かしてみるのが
大事ですね。