RustでOpenTelemetryを使う

botcast-worker でスクリプト内の引数やOpenAI APIの所要時間等を知りたかったので OpenTelemetry を利用した。

log crate

Rustでは log crateを使ってロギングを行う。I/Fと実装は分かれている(I/F: log crate, 実装: env_logger crate, fern crate 等)。

fn say_hello(id: u32) {
    log::info!("Hello, {}", id); // Event
}
 
fn main() {
    env_logger::init(); // 初期化
    say_hello(123); // 使用
}

tracing crate

コンテキスト(HTTPリクエスト情報等)を含むロギングは tracing crateを使う。

  • Span: 処理を含む期間でコンテキスト情報を持てる
  • Event: Span に記録するトレースしたい事象
  • Subscriber: SpanEvent を収集する処理
#[tracing::instrument(skip(key))] // Span: say_hello
fn say_hello(key: &str, id: u32) {
    tracing::info!("Hello, {} key={}", id, key); // Event
}
 
fn main() {
    tracing_subscriber::fmt().init(); // 初期化
    say_hello("secret", 123); // 使用
}

参考: tracing crateを利用したRustのlogging方法について

OpenTelemetry概要

Observability

  • システムを調査するには、アプリケーションが適切に instrumented されている必要がある
    • instrumented されている = アプリケーションがSignals(traces, metrics, logs等)を発していること
  • → 問題発生時にアプリケーションに追加の変更を加えることなく調査が可能になる。なぜなら、調査に必要な情報は全て収集されているから

OpenTelemetry

  • アプリケーションを instrumented にするための仕組み
    • プログラミング言語に依存しない仕様、言語ごとのSDK
  • 4つのSignalsから成る: Traces(ここでの話題) / Metrics / Logs / Baggage

構成

  • app: OpenTelemetry Collector(otel-collector:4317)にトレース情報を送る
  • opentelemetry-collector: :4317 でappからトレース情報を受け取り、jaeger (jaeger:5000) に送信する
  • jaeger: 可視化バックエンド

アプリ側の設定

  • まずはJSONで出す
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["json"] }
 
tracing_subscriber::fmt()
    .json()
    .with_max_level(tracing::Level::INFO)
    .init();
  • OTLP exporter を足す
fn init_tracing(otlp_collector_endpoint: String) -> anyhow::Result<()> {
    let subscriber = tracing_subscriber::registry();
    let exporter = opentelemetry_otlp::SpanExporter::builder()
        .with_tonic()
        .with_endpoint(otlp_collector_endpoint)
        .build()?;
    let tracer_provider = TracerProvider::builder()
        .with_batch_exporter(exporter, Tokio)
        .with_resource(Resource::new(vec![KeyValue::new(
            "service.name",
            crate_name.to_string(),
        )]))
        .build();
    let otel_layer = OpenTelemetryLayer::new(tracer_provider.tracer("worker"));
    let subscriber = subscriber
        .with(otel_layer)
        .with(EnvFilter::from_str(&format!("info,{}=trace", crate_name))?);
    let fmt_layer = tracing_subscriber::fmt::layer().pretty();
    let subscriber = subscriber.with(fmt_layer);
    tracing::subscriber::set_global_default(subscriber)?;
    Ok(())
}

結果

標準出力

スクリプト内で呼ばれた関数の引数も捕捉できている

未調査:

  • LangfuseにもLLMのトレースを行う機能(Ingestion API)があり、OTelとの互換性等
  • メトリクスとしてTraceを加工したり、アプリ側でTrace情報を取得する方法

セットアップのメモ

go install github.com/equinix-labs/otel-cli@latest
 
otel-cli exec --name "test" --protocol grpc --endpoint http://localhost:4317 --verbose --tp-print echo 1
1
# trace id: 8aaf4d2ca62204f400ae88705fa55afb
#  span id: 8430124b37cd2a50
TRACEPARENT=00-8aaf4d2ca62204f400ae88705fa55afb-8430124b37cd2a50-01
otel-collector-1  | 2024-11-30T09:34:29.355Z    error   internal/queue_sender.go:92     Exporting failed. Dropping data.        {"kind": "exporter", "data_type": "traces", "name": "otlp", "error": "not retryable error: Permanent error: rpc error: code = Unimplemented desc = unknown service opentelemetry.proto.collector.trace.v1.TraceService", "dropped_items": 1}
otel-collector-1  | go.opentelemetry.io/collector/exporter/exporterhelper/internal.NewQueueSender.func1
otel-collector-1  |     go.opentelemetry.io/collector/exporter@v0.114.0/exporterhelper/internal/queue_sender.go:92
otel-collector-1  | go.opentelemetry.io/collector/exporter/internal/queue.(*Consumers[...]).Start.func1
otel-collector-1  |     go.opentelemetry.io/collector/exporter@v0.114.0/internal/queue/consumers.go:43
  • otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        # ref: https://github.com/open-telemetry/opentelemetry-rust/issues/861#issuecomment-2408304710
        endpoint: 0.0.0.0:4317
 
exporters:
  debug:
  otlp:
    endpoint: jaeger:5000
    tls:
      insecure: true
 
processors:
  batch:
 
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp, debug]
 
  • compose.yaml
services:
  jaeger:
    image: "jaegertracing/all-in-one:latest"
    ports:
      - "5000:5000" # gRPC server
      - "16686:16686" # Web UI
    environment:
      - COLLECTOR_OTLP_ENABLED=true
    command:
      # ref: https://zenn.dev/hkdord/articles/oss-reading-jaeger
      - "--collector.otlp.grpc.host-port=5000"
    networks:
      - langfuse-network
  otel-collector:
    image: otel/opentelemetry-collector:latest
    restart: always
    command: ["--config=/etc/otel-collector-config.yaml", ""]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - "4317:4317"
    networks:
      - langfuse-network
    depends_on:
      - jaeger
 
networks:
  langfuse-network:
    driver: bridge
 

参考文献