CLOVER🍀

That was when it all began.

PrometheusでOpenTelemetryのメトリクスシグナルを直接受け取る

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

Prometheusといえばpull型のメトリクス収集ソフトウェアですが、リモート書き込みの他にOpenTelemetryのメトリクスシグナルを
直接受け取ることもできるようです。

ちょっと試してみましょう。

PrometheusをOpenTelemetryのバックエンドとして使う

ドキュメントとしてはこちらです。

Using Prometheus as your OpenTelemetry backend | Prometheus

デフォルトでは無効になっていますが、起動時に--web.enable-otlp-receiverというフラグを付与することでPrometheusでOpenTelemetryの
メトリクスを受け取ることができます。

デフォルトで無効なのは、Prometheusには認証の仕組みがないからですね。

ポイントはこのあたりでしょうか。

後半2つがちょっとよくわかりません。属性のプロモーションについてはこちらに書いてあるのですが、どうもGrafanaで見る時に扱いにくかった
みたいですね。

OpenTelemetry with Prometheus: better integration through resource attribute promotion | Grafana Labs

今回、このあたりも少し試してみましょう。

環境

今回の環境はこちら。

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のメトリクスシグナルを直接受け取れることを確認してみました。

またドキュメントに書かれている設定の意味も確認できました。最初はよくわからなかったのですが、こういうのはやっぱり動かしてみるのが
大事ですね。