From ce280fdc6c95aeeb48eb7765006a6a2a6e8c2622 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Mon, 18 May 2026 20:01:55 +0700 Subject: [PATCH 1/2] chore: stdout for span exporter if env OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_TRACES_ENDPOINT set to `stdout` export the traces to stdout --- Cargo.lock | 12 +++++++ Cargo.toml | 1 + src/telemetry.rs | 94 +++++++++++++++++++++++++++--------------------- 3 files changed, 67 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b455f4067..0f1361659 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3748,6 +3748,17 @@ dependencies = [ "tonic-prost", ] +[[package]] +name = "opentelemetry-stdout" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc8887887e169414f637b18751487cce4e095be787d23fad13c454e2fb1b3811" +dependencies = [ + "chrono", + "opentelemetry 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", + "opentelemetry_sdk 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "opentelemetry_sdk" version = "0.31.0" @@ -3919,6 +3930,7 @@ dependencies = [ "opentelemetry 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", "opentelemetry-otlp", "opentelemetry-proto 0.31.0 (git+https://github.com/open-telemetry/opentelemetry-rust/?rev=b096b70b2ffe9beb65a716cf47d5e5db80a9e930)", + "opentelemetry-stdout", "opentelemetry_sdk 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot", "parquet", diff --git a/Cargo.toml b/Cargo.toml index 976c1ee4f..f8c016042 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,6 +127,7 @@ opentelemetry-otlp = { version = "0.31.1", features = [ "http-proto", "http-json", ] } +opentelemetry-stdout = "0.31.0" tracing-actix-web = "0.7" # Time and Date diff --git a/src/telemetry.rs b/src/telemetry.rs index 7c67e68f3..e0f6d7c7f 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -33,7 +33,8 @@ const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL"; /// Initialise an OTLP tracer provider. /// /// **Required env var:** -/// - \`OTEL_EXPORTER_OTLP_ENDPOINT\` — collector address. +/// - \`OTEL_EXPORTER_OTLP_ENDPOINT\` — collector address, or the literal +/// value \`stdout\` to write spans as JSON to process stdout (no collector). /// For HTTP exporters the SDK appends the signal path automatically: /// e.g. \`http://localhost:4318\` → \`http://localhost:4318/v1/traces\`. /// Set a signal-specific var \`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT\` to @@ -44,6 +45,7 @@ const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL"; /// - \`grpc\` → gRPC / tonic (Jaeger, Tempo, …) /// - \`http/json\` → HTTP + JSON (Parseable OSS ingest at \`/v1/traces\`) /// - \`http/protobuf\` → HTTP + protobuf +/// Ignored when endpoint is \`stdout\`. /// - \`OTEL_EXPORTER_OTLP_HEADERS\` — comma-separated \`key=value\` pairs forwarded /// as gRPC metadata or HTTP headers, e.g. /// \`authorization=Basic ,x-p-stream=my-stream,x-p-log-source=otel-traces\` @@ -51,52 +53,66 @@ const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL"; /// Returns \`None\` when \`OTEL_EXPORTER_OTLP_ENDPOINT\` or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` is not set (OTEL disabled). /// The caller must call \`provider.shutdown()\` before process exit. pub fn init_tracing(service: &'static str) -> Option { - // Only used to decide whether OTEL is enabled; the SDK reads it again - // from env to build the exporter (which also appends /v1/traces for HTTP). - if std::env::var(OTEL_EXPORTER_OTLP_ENDPOINT).is_err() - && std::env::var(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT).is_err() - { - return None; - } + let endpoint = std::env::var(OTEL_EXPORTER_OTLP_ENDPOINT) + .or_else(|_| std::env::var(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)) + .ok()?; + + // Endpoint sentinel: route spans to stdout, skip OTLP exporter entirely. + let use_stdout = endpoint.eq_ignore_ascii_case("stdout"); let protocol = std::env::var(OTEL_EXPORTER_OTLP_PROTOCOL).unwrap_or_else(|_| "http/json".to_string()); - // Build the exporter using the SDK's env-var-aware builders. + // Build the exporter and wrap it in a BatchSpanProcessor. The stdout + // exporter has a different concrete type, so we build the processor + // inside each match arm to keep the types monomorphic. + // // We intentionally do NOT call .with_endpoint() / .with_headers() / - // .with_metadata() here — the SDK reads OTEL_EXPORTER_OTLP_ENDPOINT and - // OTEL_EXPORTER_OTLP_HEADERS from the environment automatically, which + // .with_metadata() on OTLP builders — the SDK reads OTEL_EXPORTER_OTLP_ENDPOINT + // and OTEL_EXPORTER_OTLP_HEADERS from the environment automatically, which // preserves correct path-appending behaviour for HTTP exporters. - let exporter = match protocol.as_str() { - // ── gRPC ───────────────────────────────────────────────────────────── - "grpc" => opentelemetry_otlp::SpanExporter::builder() - .with_tonic() - .build(), - // ── HTTP/Protobuf ──────────────────────────────────────────────────── - "http/protobuf" => SpanExporter::builder() - .with_http() - .with_protocol(Protocol::HttpBinary) - .build(), - // ── HTTP/JSON ───────────────────────────────────────────────────────── - "http/json" => SpanExporter::builder() - .with_http() - .with_protocol(Protocol::HttpJson) - .build(), - // return none if an invalid value is set - other => { - tracing::warn!( - "Unknown OTEL_EXPORTER_OTLP_PROTOCOL value '{}'; disabling OTEL tracing. \ - Supported values: grpc, http/protobuf, http/json", - other - ); - return None; + let processor = if use_stdout { + let exporter = opentelemetry_stdout::SpanExporter::default(); + BatchSpanProcessor::builder(exporter).build() + } else { + match protocol.as_str() { + "grpc" => { + let exporter = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .build() + .map_err(|e| tracing::warn!("Failed to build OTEL span exporter: {}", e)) + .ok()?; + BatchSpanProcessor::builder(exporter).build() + } + "http/protobuf" => { + let exporter = SpanExporter::builder() + .with_http() + .with_protocol(Protocol::HttpBinary) + .build() + .map_err(|e| tracing::warn!("Failed to build OTEL span exporter: {}", e)) + .ok()?; + BatchSpanProcessor::builder(exporter).build() + } + "http/json" => { + let exporter = SpanExporter::builder() + .with_http() + .with_protocol(Protocol::HttpJson) + .build() + .map_err(|e| tracing::warn!("Failed to build OTEL span exporter: {}", e)) + .ok()?; + BatchSpanProcessor::builder(exporter).build() + } + other => { + tracing::warn!( + "Unknown OTEL_EXPORTER_OTLP_PROTOCOL value '{}'; disabling OTEL tracing. \ + Supported values: grpc, http/protobuf, http/json", + other + ); + return None; + } } }; - let exporter = exporter - .map_err(|e| tracing::warn!("Failed to build OTEL span exporter: {}", e)) - .ok()?; - // Declare conformance to OTel Semantic Conventions v1.56.0 via schema_url. // Downstream collectors (e.g., the Schema Translate Processor) can apply // migration tables to rewrite attribute names across semconv versions — @@ -109,8 +125,6 @@ pub fn init_tracing(service: &'static str) -> Option { ) .build(); - let processor = BatchSpanProcessor::builder(exporter).build(); - let provider = SdkTracerProvider::builder() .with_span_processor(processor) .with_resource(resource) From 6e472d4ca82683bfa9da144772b8bc013b7d01a3 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Mon, 18 May 2026 23:02:22 +0700 Subject: [PATCH 2/2] stdout if either endpoint is set to stdout --- src/telemetry.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/telemetry.rs b/src/telemetry.rs index e0f6d7c7f..431d4938a 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -53,12 +53,19 @@ const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL"; /// Returns \`None\` when \`OTEL_EXPORTER_OTLP_ENDPOINT\` or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` is not set (OTEL disabled). /// The caller must call \`provider.shutdown()\` before process exit. pub fn init_tracing(service: &'static str) -> Option { - let endpoint = std::env::var(OTEL_EXPORTER_OTLP_ENDPOINT) - .or_else(|_| std::env::var(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)) - .ok()?; + let endpoint = std::env::var(OTEL_EXPORTER_OTLP_ENDPOINT).ok(); + let traces_endpoint = std::env::var(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT).ok(); + if endpoint.is_none() && traces_endpoint.is_none() { + return None; + } // Endpoint sentinel: route spans to stdout, skip OTLP exporter entirely. - let use_stdout = endpoint.eq_ignore_ascii_case("stdout"); + // Either env var set to "stdout" (case-insensitive) triggers stdout mode. + let is_stdout_sentinel = |v: &Option| { + v.as_deref() + .is_some_and(|s| s.eq_ignore_ascii_case("stdout")) + }; + let use_stdout = is_stdout_sentinel(&endpoint) || is_stdout_sentinel(&traces_endpoint); let protocol = std::env::var(OTEL_EXPORTER_OTLP_PROTOCOL).unwrap_or_else(|_| "http/json".to_string());