配送サービス

このサービスは、チェックアウトサービスからリクエストされた際に、料金やトラッキング情報を含む配送情報を提供する役割を担います。

配送サービスは Actix Web と、ログのための Tracing、および OpenTelemetry ライブラリを使用して構築されています。 その他のサブ依存関係はすべて Cargo.toml に含まれています。

フレームワークやランタイムによっては、補足として Rust のドキュメントを参照することを検討してください。 見積もりリクエストとトラッキング ID において、非同期および同期のスパンの例をそれぞれ確認できます。

配送サービスのソースコード

計装

OpenTelemetry SDK は telemetry_conf ファイルで設定されています。

デフォルトのリソース検出器に加えて OSProcess 検出器を使用してリソースを作成するための関数 get_resource() が実装されています。

fn get_resource() -> Resource {
    let detectors: Vec<Box<dyn ResourceDetector>> = vec![
        Box::new(OsResourceDetector),
        Box::new(ProcessResourceDetector),
    ];

    Resource::builder().with_detectors(&detectors).build()
}

get_resource() を用意することで、この関数をすべてのプロバイダー初期化にわたって複数回呼び出すことができます。

トレーサープロバイダーの初期化

fn init_tracer_provider() {
    global::set_text_map_propagator(TraceContextPropagator::new());

    let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
        .with_resource(get_resource())
        .with_batch_exporter(
            opentelemetry_otlp::SpanExporter::builder()
                .with_tonic()
                .build()
                .expect("Failed to initialize tracing provider"),
        )
        .build();

    global::set_tracer_provider(tracer_provider);
}

メータープロバイダーの初期化

fn init_meter_provider() -> opentelemetry_sdk::metrics::SdkMeterProvider {
    let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
        .with_resource(get_resource())
        .with_periodic_exporter(
            opentelemetry_otlp::MetricExporter::builder()
                .with_temporality(opentelemetry_sdk::metrics::Temporality::Delta)
                .with_tonic()
                .build()
                .expect("Failed to initialize metric exporter"),
        )
        .build();
    global::set_meter_provider(meter_provider.clone());

    meter_provider
}

ロガープロバイダーの初期化

ログについては、配送サービスは Tracing を使用しているため、tracing クレートから OpenTelemetry へログをブリッジするために OpenTelemetryTracingBridge が使用されています。

fn init_logger_provider() {
    let logger_provider = opentelemetry_sdk::logs::SdkLoggerProvider::builder()
        .with_resource(get_resource())
        .with_batch_exporter(
            opentelemetry_otlp::LogExporter::builder()
                .with_tonic()
                .build()
                .expect("Failed to initialize logger provider"),
        )
        .build();

    let otel_layer = OpenTelemetryTracingBridge::new(&logger_provider);
    let filter_otel = EnvFilter::new("info");
    let otel_layer = otel_layer.with_filter(filter_otel);

    tracing_subscriber::registry().with(otel_layer).init();
}

計装の初期化

トレース、メトリクス、ログのプロバイダーを初期化する関数を定義した後、パブリック関数 init_otel() が作成されます。

pub fn init_otel() -> Result<()> {
    init_logger_provider();
    init_tracer_provider();
    init_meter_provider();
    Ok(())
}

この関数はすべての初期化関数を呼び出し、すべてが正常に開始された場合に OK(()) を返します。

init_otel() 関数は main で呼び出されます。

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    match init_otel() {
        Ok(_) => {
            info!("Successfully configured OTel");
        }
        Err(err) => {
            panic!("Couldn't start OTel: {0}", err);
        }
    };

    [...]

}

計装の設定

プロバイダーの設定と初期化が完了したため、配送サービスはサーバーサイドおよびクライアントサイドの設定中にアプリケーションを計装するために opentelemetry-instrumentation-actix-web クレートを使用します。

サーバーサイド

サーバーは RequestTracingRequestMetrics でラップされ、リクエストを受信した際にトレースとメトリクスが自動的に作成されます。

HttpServer::new(|| {
    App::new()
        .wrap(RequestTracing::new())
        .wrap(RequestMetrics::default())
        .service(get_quote)
        .service(ship_order)
})

クライアントサイド

別のサービスにリクエストを送信する際、trace_request() が呼び出しに追加されます。

let mut response = client
    .post(quote_service_addr)
    .trace_request()
    .send_json(&reqbody)
    .await
    .map_err(|err| anyhow::anyhow!("Failed to call quote service: {err}"))?;

手動計装

opentelemetry-instrumentation-actix-web クレートを使用すると、前のセクションで説明したコマンドを追加することで、サーバーサイドとクライアントサイドの計装が可能になります。

デモでは、自動的に作成されたスパンを手動で拡張する方法と、アプリケーションで手動のメトリクスを作成する方法も紹介しています。

手動スパン

次のスニペットでは、現在のアクティブなスパンにスパンイベントとスパン属性を追加して拡張しています。

Ok(get_active_span(|span| {
    let q = create_quote_from_float(f);
    span.add_event(
        "Received Quote".to_string(),
        vec![KeyValue::new("app.shipping.cost.total", format!("{}", q))],
    );
    span.set_attribute(KeyValue::new("app.shipping.cost.total", format!("{}", q)));
    q
}))

手動メトリクス

配送リクエストに含まれるアイテム数をカウントするためのカスタムメトリクスカウンターが作成されます。

let meter = global::meter("otel_demo.shipping.quote");
let counter = meter.u64_counter("app.shipping.items_count").build();
counter.add(count as u64, &[]);

ログ

配送サービスはログインターフェイスとして Tracing を使用しているため、Tracing のログを OpenTelemetry のログにブリッジするために opentelemetry-appender-tracing クレートを使用しています。

アペンダーはロガープロバイダーの初期化時に、次の 2 行ですでに設定されています。

let otel_layer = OpenTelemetryTracingBridge::new(&logger_provider);
tracing_subscriber::registry().with(otel_layer).init();

これにより、通常どおり Tracing を使用できます。 たとえば次のようになります。

info!(
    name = "SendingQuoteValue",
    quote.dollars = quote.dollars,
    quote.cents = quote.cents,
    message = "Sending Quote"
);

opentelemetry-appender-tracing クレートがログエントリに OpenTelemetry のコンテキストを追加し、最終的にエクスポートされたログには設定されたすべてのリソース属性と TraceContext 情報が含まれます。