0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

New Relic Transactionsが空だった原因はスパンにhttp.routeがなかったから

0
Posted at

はじめに

以前の記事「New Relicで実現するRust・Axumアプリのトレースとログ管理」では、Rust(Axum)アプリに OpenTelemetry 経由で New Relic を導入し、ログ・トレース・トランザクションの取得までを紹介しました。

本記事はその続編です。前回の構成で本ブログ(Leptos + Axum)を運用していたところ、NR を導入した当初は Transactions 画面にルート別のトランザクションが正しく表示されており、問題なく動作していました。

ところがある日、Transactions 画面を開くと Transaction Traces が一切紐づかなくなっていることに気づきました。調べてみると、NR が 2025年に OpenTelemetry サービスの APM 体験を刷新し(APM + OpenTelemetry Convergence)、Transactions 画面のデフォルトが metric ベースに切り替わっていました。旧来の span ベースの画面は「Span view (Legacy)」に移動しており、こちらは段階的に廃止される予定です(ドキュメント)。Span view (Legacy) では以前と変わらずトランザクションが表示されていたため、スパン自体は正しく送れていましたが、新しい metric ベースの Transactions 画面にはトレースが紐づかない状態でした。

本記事では、この問題の原因調査から修正までを紹介します。

症状

metric ベースに移行後の Transactions 画面に表示されていたのは以下の状態です。

  • GET /, GET /articles/{id}, GET /auth/google 等 → Transaction Traces なし
  • WebTransaction/server/GET → 全リクエストのトレースがここに集約

また、NR APM の画面上部には以下の警告バナーが表示されていました。

image.png

ルート別の Transaction はメトリクスから生成されて存在するのにトレースが空、逆にトレースは全部 GET という1つの Transaction に集まっている。メトリクスとスパンで Transaction 名の決定に使われる属性が噛み合っていませんでした。

なお、Span view (Legacy) では従来どおり otel.name をもとにトランザクションが表示されており、こちらには問題がありませんでした。

image.png

Span view (Legacy) の Transactions タブ。otel.name ベースでルート別にトランザクションが表示されている

原因:スパンに http.route がなかった

NR の Transaction 名はどう決まるか

NR の OpenTelemetry 連携では、HTTP server スパンの Transaction 名を以下のフォーマットで導出します(公式ドキュメント)。

WebTransaction/server/${http.request.method} ${http.route}

旧セマンティクス(HTTP semconv 1.20)の場合は ${http.method} が使われます。

ここで重要なのは、otel.name(OpenTelemetry のスパン名)は Transaction 名には使われないということです。NR はスパンの属性 http.request.methodhttp.route から Transaction 名を生成します。

image.png

Transaction 名の決定フロー: 旧(spanベース)vs 新(metricベース)

修正前のコード

前回の記事で紹介した MakeSpanForHttp をそのまま使っていました。tower-httpTraceLayer にカスタムの MakeSpan 実装を渡す構成です。

impl<B> MakeSpan<B> for MakeSpanForHttp {
    fn make_span(&mut self, request: &Request<B>) -> Span {
        let matched_path = request
            .extensions()
            .get::<MatchedPath>()
            .map_or(request.uri().to_string(), |m| m.as_str().to_string());
        tracing::info_span!(
            "request",
            http.method = %request.method(),
            http.uri = %request.uri(),
            http.version = ?request.version(),
            otel.name = format!("{} {}", request.method(), matched_path),
            otel.kind = "server",
        )
    }
}

問題は2つあります。

  1. http.route がない — NR が Transaction をルート別に分類できない
  2. http.method は旧セマンティクス — 新しい OTel HTTP セマンティクス(1.23+)では http.request.method が正しい

otel.name には GET /articles/{id} のような値が入っていました。旧来の span ベースの Transactions 画面(現 Span view (Legacy))ではこの otel.name から Transaction 名が導出されていたため、前回の記事の構成でも問題なく動作していました。しかし metric ベースに移行した現在の Transactions 画面では otel.name は使われず、http.request.method + http.route から Transaction 名が生成されます。

メトリクスもそもそも送れていなかった

実はスパンの属性不足に加えて、http.server.request.duration メトリクス自体が NR に送信されていませんでした。

前回の記事でも「なぜかメトリクスが送られていない」と記していましたが、原因は OTel 初期化用の自作クレート easy_init_newrelic_opentelemetrySdkMeterProvider をグローバルにセットしていなかったことでした。メトリクスプロバイダーが未設定のため、アプリケーション側でいくら Histogram::record() を呼んでも NR には何も届いていなかったわけです。

つまり修正前の状態は以下のとおりです。

  1. スパン: http.route なし、otel.name のみ → 旧 span ベースでは動作していたが metric ベースでは不十分
  2. メトリクス: そもそも送信されていない → metric ベースの Transactions 画面が機能しない

metric ベースの Transactions を正しく動作させるには、メトリクスの送信基盤とスパンの属性の両方を修正する必要がありました。

背景:NR の Transactions が metric ベースに移行した経緯

NR は段階的に OpenTelemetry サービスの APM 体験を span ベースから metric ベースに移行してきました。

  • 2022年10月: Transactions 画面に metric ベースのビューを追加。metric/span の切り替えトグルが導入された(What's New, 2022-10-14
  • 2025年1月: APM ゴールデンメトリクス(latency, error rate, throughput)の算出が span データから metrics データに移行(What's New, 2025-01-13
  • 2025年7月: APM + OpenTelemetry Convergence により、OpenTelemetry サービスの APM 体験が NR 言語エージェントと統一。旧 span ベースの画面は「Span view (Legacy)」に移動(ブログ

移行の理由は、span データはサンプリングされるため、スループットやレスポンスタイムの集約値が不正確になるという問題です。metric ベースでは http.server.request.duration ヒストグラムから正確なパフォーマンス統計を算出できます。

現在の Transactions 画面はこの metric ベースがデフォルトで、旧来の span ベースは Span view (legacy) として残っていますが、段階的に廃止される予定です。今後 OpenTelemetry サービスを運用するなら metric ベースへの対応は必須です。

metric ベースの Transactions を正しく機能させるには以下の両方が必要です。

  1. http.server.request.duration メトリクスに http.route を含める(Transaction 名の生成)
  2. トレーシングスパンにも同じ http.route を含める(トレースの紐づけ)

修正

修正は大きく2つです。

  1. メトリクス記録とスパン生成を行うミドルウェアの追加
  2. easy_init_newrelic_opentelemetry の更新(SdkMeterProvider のグローバルセット)

2 については後述の easy_init_newrelic_opentelemetry の更新 で説明します。

メトリクスとスパンを統合したミドルウェア

修正前は MakeSpanForHttp(スパン生成)のみで、メトリクス記録はありませんでした。修正後は http.server.request.duration メトリクスの記録とスパン生成を1つのミドルウェア http_observability に統合しました。

static HTTP_SERVER_REQUEST_DURATION: LazyLock<Histogram<f64>> = LazyLock::new(|| {
    opentelemetry::global::meter("http")
        .f64_histogram("http.server.request.duration")
        .with_description("Duration of HTTP server requests.")
        .with_unit("s")
        .build()
});

async fn http_observability(req: Request<axum::body::Body>, next: Next) -> Response {
    let start = Instant::now();
    let method = req.method().clone();
    let url_path = req.uri().path().to_string();
    let url_scheme = req.uri().scheme_str().unwrap_or("http").to_string();
    let route = req
        .extensions()
        .get::<MatchedPath>()
        .map(|m| m.as_str().to_string())
        .unwrap_or_else(|| "UNMATCHED".to_string());

    let span = tracing::info_span!(
        "request",
        http.request.method = %method,
        http.route = %route,
        url.path = %url_path,
        url.scheme = %url_scheme,
        http.response.status_code = tracing::field::Empty,
        otel.name = format!("{} {}", method, route),
        otel.kind = "server",
    );

    let response = next.run(req).instrument(span.clone()).await;

    let status = response.status().as_u16();
    span.record("http.response.status_code", i64::from(status));

    let duration = start.elapsed().as_secs_f64();
    HTTP_SERVER_REQUEST_DURATION.record(
        duration,
        &[
            KeyValue::new("http.request.method", method.to_string()),
            KeyValue::new("http.route", route),
            KeyValue::new("http.response.status_code", i64::from(status)),
        ],
    );
    response
}

修正前の MakeSpanForHttp からの主な変更点は以下のとおりです。

  • http.methodhttp.request.method に変更(OTel HTTP semconv 1.23+
  • http.route を追加(MatchedPath から取得)
  • url.pathurl.scheme を追加(OTel セマンティクスで Required)
  • http.response.status_code をレスポンス後にスパンに記録
  • http.server.request.duration メトリクスを同じ http.route 値で記録
  • ルートにマッチしなかったリクエストの fallback を "UNMATCHED" 固定値に設定(URI パスを使うと高カーディナリティになるため)

Axum の MatchedPath はレイヤーから取得できるのか

Axum 0.8 では Router::layer() で適用したミドルウェアから MatchedPath を取得できます。これは直感に反するかもしれませんが、Axum 0.8 の Router::layer() は Router を外側からラップするのではなく、内部のルートハンドラにレイヤーを適用する実装になっています。

// axum-0.8.8/src/routing/mod.rs
pub fn layer<L>(self, layer: L) -> Router<S> {
    map_inner!(self, this => RouterInner {
        path_router: this.path_router.layer(layer.clone()),
        fallback_router: this.fallback_router.layer(layer.clone()),
        catch_all_fallback: this.catch_all_fallback.map(|route| route.layer(layer)),
    })
}

ルーティング(MatchedPath の設定)→ レイヤー実行 → ハンドラという順序になるため、レイヤー内で MatchedPath が参照できます。ただし fallback ハンドラ(ルートにマッチしなかったリクエスト)では MatchedPath は設定されません。

OTel HTTP セマンティクスの整理

OpenTelemetry Semantic Conventions for HTTP では、HTTP server スパンに以下の属性が定義されています。

属性名 必須/推奨 説明
http.request.method Required HTTP メソッド(GET, POST 等)
http.route Conditionally Required マッチしたルートテンプレート(/users/{id} 等)
url.path Required URI パス
url.scheme Required http / https

スパン名の推奨フォーマットは {method} {http.route} です。

旧セマンティクス(v1.20.0)からの主な変更は以下のとおりです。

http.method http.request.method
http.status_code http.response.status_code
http.url url.full

http.route は新旧で変更なしです。

http.server.request.duration メトリクスにも同様に http.request.method(Required)と http.route(Conditionally Required)が定義されています。http.route はフレームワークから取得可能な場合にのみ設定し、低カーディナリティであるべきとされています。URI パスで代替してはなりません。

easy_init_newrelic_opentelemetry の更新

今回の対応の一環として、前回の記事でも紹介した OTel 初期化用の自作クレート easy_init_newrelic_opentelemetry を更新しました。

0.4.2 → 0.5.0

前述のとおり、メトリクスが NR に送信されていなかった原因はこのクレートにありました。SdkMeterProvider をグローバルにセットしていなかったため、アプリケーション側で Histogram::record() を呼んでもメトリクスが OTLP エンドポイントに送信されませんでした。NR APM にも "Required metrics are missing" という警告が出ていました。

この修正でメトリクスプロバイダー(SdkMeterProvider)とプロセスメトリクスを追加しました。BatchSpanProcessor のパニック修正(reqwest-clientreqwest-blocking-client)も含まれています。

0.5.0 → 0.6.0

主な変更は以下のとおりです。

  • anyhowthiserror に移行(エラー型の明確化)
  • TelemetryGuard の導入 — init() の戻り値が Result<TelemetryGuard, Error> に変更。TelemetryGuard を保持している間はテレメトリが有効で、Drop 時に graceful shutdown される
  • ライセンスキー未設定時に OTLP エクスポーターをスキップ(ローカル開発で NR 接続なしでも起動可能に)
  • grpc-tonic feature 削除(HTTP のみに簡素化)
  • パッチ版 opentelemetry-appender-tracing2 を廃止前回の記事では、ログに trace.id/span.id を付与するために opentelemetry-appender-tracing にパッチを当てた自作クレートを使っていた。その後、公式の opentelemetry-appender-tracingexperimental_use_tracing_span_context feature が追加され、同等の機能が提供されるようになったため、パッチ版への依存を削除した
// 0.5.0 以前
easy_init_newrelic_opentelemetry::NewRelicSubscriberInitializer::default()
    .newrelic_service_name(&SERVER_CONFIG.new_relic_service_name)
    // ...
    .init()
    .expect("Failed to initialize NewRelic");

// 0.6.0 以降(TelemetryGuard を保持する必要がある)
let _telemetry_guard =
    easy_init_newrelic_opentelemetry::NewRelicSubscriberInitializer::default()
        .newrelic_service_name(&SERVER_CONFIG.new_relic_service_name)
        // ...
        .init()
        .expect("Failed to initialize NewRelic");

修正後の Transactions 画面

修正後、metric ベースの Transactions 画面でルート別にトランザクションが正しく表示され、Transaction Traces も紐づくようになりました。

image.png

修正後の Transactions 画面(metric ベース)。GET /, GET /articles/{id} 等ルート別に表示され、Transaction Traces も紐づいている

まとめ

NR の Transactions にトレースが表示されない原因は、以下の2点でした。

  1. メトリクスが送信されていなかったeasy_init_newrelic_opentelemetrySdkMeterProvider をグローバルセットしておらず、http.server.request.duration が NR に届いていなかった
  2. スパンに http.route がなかった — metric ベースの Transactions 画面ではスパンの http.request.method + http.route からトレースを Transaction に紐づけるが、この属性が欠落していた

旧来の span ベースでは otel.name(スパン名)から Transaction 名が導出されていたため問題にならなかったが、NR の APM + OpenTelemetry Convergence により metric ベースがデフォルトになったことで顕在化しました。

対応としては以下の2点です。

  1. メトリクス記録(http.server.request.duration)とスパン生成(http.route + http.request.method)を統合したミドルウェアの追加
  2. easy_init_newrelic_opentelemetry の更新(SdkMeterProvider のグローバルセット)

OTel のセマンティクスに従ってメトリクスとスパンの属性を正しく設定すれば、NR 側で特別な設定は不要です。Span view (Legacy) は段階的に廃止予定のため、今後の対応は必須です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?