New RelicではOpenTelemetry(以下OTel)で収集したテレメトリーデータを受け取り、APMと同じようにトランザクション単位でパフォーマンスを可視化できます。しかし、PythonアプリにOTelを導入してNew Relicに送信してみたものの、トランザクションがHTTPメソッド単位でまとまってしまい、パスが表示されないことがあります。

原因の一つとして、送信メトリクスに、New Relicがパスの判定に使う属性(http.route)が含まれていないことが考えられます。 本記事では、このトランザクション名にパスが表示されないケースの原因特定から対処までの流れを確認していきます。

前提

本記事では、アプリのコードをほとんど変更せずに計装する「ゼロコード計装」で確認を行います。 ゼロコード計装の詳細については、OpenTelemetryのドキュメント「Pythonゼロコード・計装」を参照してください。

導入は、opentelemetry-distroopentelemetry-exporter-otlp をインストールし、opentelemetry-bootstrap -a install を実行します。

pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install

また、New Relicへはデータを OTLP エクスポーターで送信します。設定はNew Relic公式のサンプル集 newrelic-opentelemetry-examples に沿って、環境変数で指定します。

export OTEL_SERVICE_NAME=fastapi-newrelic-app
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net
export OTEL_EXPORTER_OTLP_HEADERS="api-key=<YOUR_LICENSE_KEY>"
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_COMPRESSION=gzip
export OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta
export OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT=4095
export OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true

OTEL_EXPORTER_OTLP_HEADERS<YOUR_LICENSE_KEY> は、ご自身のNew Relicライセンスキーに置き換えてください。 また、例ではエンドポイントにUSを指定していますが、利用しているデータセンターに応じてエンドポイントを適宜変更して下さい。

なぜパスが表示されないのか

New Relicは、OpenTelemetryのトランザクション名を、スパンではなくメトリクスの属性から組み立てています(変換ルールの詳細は New Relic ドキュメント「OpenTelemetry APM UI」を参照してください)。具体的には、メトリクスに付与されている http.routehttp.request.methodを使って、次のようなトランザクション名を生成します。

GET /users/{id}

ここで、メトリクスに http.route が含まれていないと、New Relicはルートを解決できず、メソッド単位へまとめてしまいます。

ポイントは、スパンには http.route が付いているのに、メトリクスでは欠落しているという点です。 そのため、スパンベースのレガシーなUIではルートごとに表示されます。

対処方法

対処方法として、OTEL_SEMCONV_STABILITY_OPT_IN を設定してHTTPのセマンティック規約を新しい安定版へ切り替えることで、メトリクスに http.route が含まれるようになります。値は次の2つが指定できます。

  • http : 新しい安定版の規約のみを使用します
  • http/dup : 新旧の規約を同時に出力します

なお、OTEL_SEMCONV_STABILITY_OPT_INhttp を指定すると、http.route が追加される一方で、一部のAttribute名も新しい規約へ変更されます(例: http.methodhttp.request.methodhttp.status_codehttp.response.status_code)。詳細はOpenTelemetryのドキュメント「HTTPセマンティック規約の移行ガイド」を参照してください。

そのため、カスタムダッシュボードやアラートなどでNRQLにAttribute名を直接指定している場合、意図したとおりにデータを取得できなくなるなどの影響を受ける可能性があります。設定する際は、この点もあらかじめご留意のうえ、実施をご検討ください。

送信データをデバッグログで確認する

実際に送信データをデバッグログで確認してみましょう。 メトリクスであれば、OTEL_METRICS_EXPORTER=console を指定することで、収集したメトリクスを標準出力に表示できます。

export OTEL_METRICS_EXPORTER=console
opentelemetry-instrument python app.py

何も設定していない状態(古い規約)では、duration系メトリクスの名前は http.server.duration(単位はミリ秒)となり、データポイントの属性に http.route が含まれていないことがわかります。

{
  "name": "http.server.duration",
  "unit": "ms",
  "data": {
    "data_points": [
      {
        "attributes": {
          "http.scheme": "http",
          "http.host": "127.0.0.1:8080",
          "net.host.port": 8080,
          "http.flavor": "1.1",
          "http.method": "GET",
          "http.server_name": "localhost:8080",
          "http.status_code": 200,
          "http.target": "/items/{item_id}"
        }
      }
    ]
  }
}

ルートの情報自体は http.target に入っていますが、New Relicがトランザクション名の組み立てに使うのは http.route です。旧規約のメトリクスにはその http.route が無いため、パスを解決できません。このようにUIが前提としている属性が、送信データに実際に含まれているかをコンソールログから確認することが可能です。

次に環境変数OTEL_SEMCONV_STABILITY_OPT_INを追加して起動します。

export OTEL_SEMCONV_STABILITY_OPT_IN=http
opentelemetry-instrument python app.py

コンソール出力を確認すると、メトリクスの名前が http.server.request.duration(単位は秒)に変わり、属性に http.route が含まれるようになります。

{
  "name": "http.server.request.duration",
  "unit": "s",
  "data": {
    "data_points": [
      {
        "attributes": {
          "url.scheme": "http",
          "network.protocol.version": "1.1",
          "http.request.method": "GET",
          "http.route": "/items/{item_id}",
          "http.response.status_code": 200
        }
      }
    ]
  }
}

http.route が送られるようになったことで、New Relic側でもパス単位のトランザクション名(例: /items/{item_id})が組み立てられるようになります。

13742-http.png

New Relic側のデータをNRQLで確認する

New Relicに届いたデータもNRQLで確認してみましょう。次のクエリでは、旧規約のメトリクス(http.server.duration)と新規約のメトリクス(http.server.request.duration)を並べて、ルートがどの属性に入っているかを比較できます。

SELECT metricName, `http.route`, `http.target`
FROM Metric
WHERE metricName IN ('http.server.duration', 'http.server.request.duration')
AND service.name = 'fastapi-newrelic-app'

旧規約の行では http.target にルートが入り http.route は空、新規約の行では http.route にルートが入り http.target は空、というように、ルートの入る属性が入れ替わっていることがわかります。

このように「送信側(コンソール)」と「受信側(NRQL)」の両方でデータを突き合わせると、どこで属性が欠けているのかを切り分けやすくなります。

まとめ

PythonアプリのOTel計装でトランザクションにパスが表示されないときは、OTEL_SEMCONV_STABILITY_OPT_IN=http を設定して新しい規約に切り替えると、メトリクスに http.route が含まれるようになり、パス単位でトランザクションが表示されるようになります。

また、UIで期待どおりに表示されないときは、コンソール出力で送信データを、NRQLで受信データを確認し、UIが前提とする属性が過不足なく含まれているかを突き合わせると、原因を特定しやすくなります。

なお、将来のバージョンではセマンティック規約の扱いが変わる可能性があるため、計装ライブラリをアップデートする際は、トランザクション名の挙動を一度確認しておくと安心です。