paild tech blog

法人カードのクラウド型発行・管理サービスpaild

Rust で Datadog にトレースとログを連携する

イントロ

はじめまして。ペイルドの中川です。

弊社はサーバーサイドの開発に Rust を採用しています。 運用監視ツールには Datadog を採用しているのですが、Datadog 公式の SDK には Rust が含まれていません。なんてこった。 この記事では Rust で Datadog と連携する方法を紹介します。記事中に登場するソースコードGitHub で公開していますので併せてご覧ください。Rust のバージョンは執筆時点(2023 年 4 月)で最新の 1.68.2 を使いました。

github.com

OpenTelemetry

Datadog は OpenTelemetry1 形式のテレメトリデータ(メトリクス、ログ、トレース)に対応しています。 OpenTelemetry が Rust 用 SDK を提供してくれているのでこれを使います。

次の axum を使ったシンプルなアプリにコードを追加して Datadog と連携できるようにします。

use axum::{routing, Router};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", routing::get(handler));

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn handler() -> &'static str {
    "Hello, World!"
}

やることは大きく分けて2つあります。

  1. トレーシングのセットアップ
  2. ミドルウェアの追加

それでは順番に解説していきます。

トレーシングのセットアップ

トレース情報を異なるサービスやプロセス間でやりとりするために TextMapPropagator をセットしましょう。 今回は Datadog を使うので opentelemetry_datadog クレートが提供する DatadogPropagator をセットします。 ヘッダにx-datadog-trace-idx-datadog-parent-idなどが含まれていると、ここからトレース情報を抽出してくれます。 私は使ったことありませんが、Jaeger2 や Zipkin3 用のクレートも提供されています。

opentelemetry::global::set_text_map_propagator(opentelemetry_datadog::DatadogPropagator::new());

ここからは Subscriber に Layer を追加していきます。Subscriber や Layer の機能についてこの記事では解説しません。 詳しく知りたい方はこちらの記事をお読みください。

blog.ymgyt.io

まずは Datadog に対応したトレーサーを作ります。 Tracer トレイトは opentelemetry クレートで定義されています。 トレーサーは OpenTelemetry の Span を作成します。tracing の Span とは別物なのでご注意ください。 tracing クレートと連携するために tracing-opentelemetryが提供する Layer にトレーサーを渡します。 tracing が Span を作成するタイミングで OpenTelemetry の Span も同時に作成されるようになります。 with_service_name()with_agent_endpoint()で設定する値は適宜読み替えてください。

let tracer = opentelemetry_datadog::new_pipeline()
    .with_service_name("demo")
    .with_agent_endpoint("http://localhost:8126")
    .install_batch(opentelemetry::runtime::Tokio)
    .expect("failed to initialize tracer");
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);

Datadog や OpenTelemetry とは直接は関係ないですが、ログレベルでフィルタしてくれる Layer も準備しておきましょう。ここでは info をベタガキしていますが、環境変数を読み込んでログレベルに設定する方がベターだと思います。

let filter_layer = EnvFilter::try_new("info").unwrap();

Datadog はログフォーマットを JSON にすることを推奨している4ので、ログを JSON に整形する Layer を追加します。 Datadog は予約済みの属性を定めています5。たとえば、ログメッセージはmessageという属性にセットすると全文検索用にインデックス化されるのでログを検索するときに便利です。 この Layer の具体的な実装はリポジトリjson_log_layer.rsをご覧下さい。 こちらの記事を参考にして Layer を作りました。

tech.emotion-tech.co.jp

これらの Layer を Subscriber に追加し、グローバルデフォルトな Subscriber として登録します6

Registry::default()
    .with(filter_layer)
    .with(otel_layer)
    .with(JsonLogLayer::new(Mutex::new(std::io::stdout())))
    .try_init()
    .expect("Failed to initialize tracing");

以上でトレーシングのセットアップは完了です。

ミドルウェアの追加

リクエスト毎に Span を作成するようにミドルウェアを追加します。 axum のミドルウェアは tower および tower_http のエコシステムと互換性があります。 tower_http の TraceLayer を使って Span の生成方法をカスタマイズして、ミドルウェアを追加します。 TraceLayer のmake_span_with()MakeSpanトレイトを実装した構造体を渡します。ここではMakeRootSpanWithRemoteという構造体に MakeSpan トレイトを実装しています。

#[derive(Clone)]
pub struct MakeRootSpanWithRemote {}

impl MakeRootSpanWithRemote {
    pub fn new() -> Self {
        Self {}
    }
}

impl<B> MakeSpan<B> for MakeRootSpanWithRemote {
    fn make_span(&mut self, request: &Request<B>) -> tracing::Span {
        let app_root = tracing::info_span!(
            "root",
            "dd.trace_id" = tracing::field::Empty,
            "dd.span_id" = tracing::field::Empty,

            // for OpenTelemetry
            "otel.name" = %request.uri(),
            "otel.kind" = "server",

            // for Datadog
            "span.type" = "web",
            "span.name" = %request.uri(),
            "http.url" = request.uri().to_string(),
            "http.method" = request.method().to_string(),
            "http.version" = ?request.version(),
            "http.useragent" = request
                .headers()
                .get(USER_AGENT)
                .map(|e| e.to_str().unwrap_or_default()),
            "http.status_code" = tracing::field::Empty, // must be filled with Empty in advance
        );

        let parent_cx = opentelemetry::global::get_text_map_propagator(|propagator| {
            propagator.extract(&opentelemetry_http::HeaderExtractor(request.headers()))
        });

        if parent_cx.span().span_context().is_valid() {
            let trace_id =
                u128::from_be_bytes(parent_cx.span().span_context().trace_id().to_bytes());
            let span_id = u64::from_be_bytes(parent_cx.span().span_context().span_id().to_bytes());

            app_root.set_parent(parent_cx);
            app_root.record("dd.trace_id", &trace_id);
            app_root.record("dd.span_id", &span_id);
        }

        app_root
    }
}

make_span()の最初ではルート Span を作成します。 Span は親をもつことができます。 リクエスタがすでにトレースを開始していた場合はそのトレース情報を親に設定します。

ここで TextMapPropagator が再登場します。 リクエストのヘッダからトレース情報を抽出します。 もし有効なトレース情報が含まれていた場合、ルート Span にトレース情報をセットします。

次のようにミドルウェアを登録すれば準備完了です。

let app = Router::new()
    .route("/greet", routing::get(handler))
    .layer(tracing_middleware);

起動する前にひと工夫しましょう。 Span が親子構造をなすことをわかりやすくするためにhandler()を次のように修正します。

#[instrument(skip_all)]
async fn handler() -> &'static str {
    tracing::info!("🦖🦖🦖");
    use_case().await
}

#[instrument(skip_all)]
async fn use_case() -> &'static str {
    tracing::info!("🌈🌈🌈");
    "Hello, World!"
}

instrument マクロを追加して関数に入るタイミングで Span を作成するようにしたのと、 handler から別の関数を呼ぶようにしました。 以上で準備完了です。

Datadog で確認

今回はローカルで起動しました。 Datadog へテレメトリーデータを送信するための Docker のイメージが配布されているのでそれを使います。 アプリが起動したらターミナルからcurl localhost:8080/greetを実行してみましょう。ログに次のように表示されていたら成功です。

demo           | {"level":"INFO","message":"🦖🦖🦖","dd.trace_id":16282717207599654834,"dd.span_id":1,"timestamp":1681465042904,"ts":"2023-04-14T09:37:22Z"}
demo           | {"level":"INFO","message":"🌈🌈🌈","dd.trace_id":16282717207599654834,"dd.span_id":1,"timestamp":1681465042904,"ts":"2023-04-14T09:37:22Z"}

それでは Datadog の Trace Explorer で確認しましょう。

一番長い Span がルート Span の/greetです。その子にhandler Span が、 handler Span の子にuse_caseがいます。 トレースとログを紐づけているので Logs タブにtracing::info!()で出力したログが表示されています。

まとめ

Rust で Datadog を使って分散トレーシングを実現する方法を紹介しました。 使うクレートが多いので最初は混乱しますが、一度設定してしまえば頻繁に修正することはないので時間がとれるときにガッとやってしまいましょう。


  1. https://opentelemetry.io/
  2. https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-jaeger
  3. https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-zipkin
  4. "Datadog は、カスタムパースルールの必要性を避けるために、JSON でログを出力するようにロギングライブラリを設定することを強く推奨しています。" https://docs.datadoghq.com/ja/opentelemetry/otel_logs/#%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AB%E5%90%88%E3%82%8F%E3%81%9B%E3%81%9F%E3%83%AD%E3%82%AC%E3%83%BC%E3%81%AE%E6%A7%8B%E6%88%90
  5. https://docs.datadoghq.com/ja/logs/log_configuration/attributes_naming_convention/
  6. try_init()という名前からも分かるように Error になる可能性があります。適切にエラーハンドリングしましょう。ここではエラーメッセージを出力して panic するようにしています。tracing-subscriber の tracing-log feature を有効にしているとき、すでにロガーがセットされているとエラーになります。