@@ -33,7 +33,8 @@ const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL";
3333/// Initialise an OTLP tracer provider.
3434///
3535/// **Required env var:**
36- /// - \`OTEL_EXPORTER_OTLP_ENDPOINT\` — collector address.
36+ /// - \`OTEL_EXPORTER_OTLP_ENDPOINT\` — collector address, or the literal
37+ /// value \`stdout\` to write spans as JSON to process stdout (no collector).
3738/// For HTTP exporters the SDK appends the signal path automatically:
3839/// e.g. \`http://localhost:4318\` → \`http://localhost:4318/v1/traces\`.
3940/// Set a signal-specific var \`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT\` to
@@ -44,59 +45,81 @@ const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL";
4445/// - \`grpc\` → gRPC / tonic (Jaeger, Tempo, …)
4546/// - \`http/json\` → HTTP + JSON (Parseable OSS ingest at \`/v1/traces\`)
4647/// - \`http/protobuf\` → HTTP + protobuf
48+ /// Ignored when endpoint is \`stdout\`.
4749/// - \`OTEL_EXPORTER_OTLP_HEADERS\` — comma-separated \`key=value\` pairs forwarded
4850/// as gRPC metadata or HTTP headers, e.g.
4951/// \`authorization=Basic <token>,x-p-stream=my-stream,x-p-log-source=otel-traces\`
5052///
5153/// Returns \`None\` when \`OTEL_EXPORTER_OTLP_ENDPOINT\` or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` is not set (OTEL disabled).
5254/// The caller must call \`provider.shutdown()\` before process exit.
5355pub fn init_tracing ( service : & ' static str ) -> Option < SdkTracerProvider > {
54- // Only used to decide whether OTEL is enabled; the SDK reads it again
55- // from env to build the exporter (which also appends /v1/traces for HTTP).
56- if std:: env:: var ( OTEL_EXPORTER_OTLP_ENDPOINT ) . is_err ( )
57- && std:: env:: var ( OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ) . is_err ( )
58- {
56+ let endpoint = std:: env:: var ( OTEL_EXPORTER_OTLP_ENDPOINT ) . ok ( ) ;
57+ let traces_endpoint = std:: env:: var ( OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ) . ok ( ) ;
58+ if endpoint. is_none ( ) && traces_endpoint. is_none ( ) {
5959 return None ;
6060 }
6161
62+ // Endpoint sentinel: route spans to stdout, skip OTLP exporter entirely.
63+ // Either env var set to "stdout" (case-insensitive) triggers stdout mode.
64+ let is_stdout_sentinel = |v : & Option < String > | {
65+ v. as_deref ( )
66+ . is_some_and ( |s| s. eq_ignore_ascii_case ( "stdout" ) )
67+ } ;
68+ let use_stdout = is_stdout_sentinel ( & endpoint) || is_stdout_sentinel ( & traces_endpoint) ;
69+
6270 let protocol =
6371 std:: env:: var ( OTEL_EXPORTER_OTLP_PROTOCOL ) . unwrap_or_else ( |_| "http/json" . to_string ( ) ) ;
6472
65- // Build the exporter using the SDK's env-var-aware builders.
73+ // Build the exporter and wrap it in a BatchSpanProcessor. The stdout
74+ // exporter has a different concrete type, so we build the processor
75+ // inside each match arm to keep the types monomorphic.
76+ //
6677 // We intentionally do NOT call .with_endpoint() / .with_headers() /
67- // .with_metadata() here — the SDK reads OTEL_EXPORTER_OTLP_ENDPOINT and
68- // OTEL_EXPORTER_OTLP_HEADERS from the environment automatically, which
78+ // .with_metadata() on OTLP builders — the SDK reads OTEL_EXPORTER_OTLP_ENDPOINT
79+ // and OTEL_EXPORTER_OTLP_HEADERS from the environment automatically, which
6980 // preserves correct path-appending behaviour for HTTP exporters.
70- let exporter = match protocol. as_str ( ) {
71- // ── gRPC ─────────────────────────────────────────────────────────────
72- "grpc" => opentelemetry_otlp:: SpanExporter :: builder ( )
73- . with_tonic ( )
74- . build ( ) ,
75- // ── HTTP/Protobuf ────────────────────────────────────────────────────
76- "http/protobuf" => SpanExporter :: builder ( )
77- . with_http ( )
78- . with_protocol ( Protocol :: HttpBinary )
79- . build ( ) ,
80- // ── HTTP/JSON ─────────────────────────────────────────────────────────
81- "http/json" => SpanExporter :: builder ( )
82- . with_http ( )
83- . with_protocol ( Protocol :: HttpJson )
84- . build ( ) ,
85- // return none if an invalid value is set
86- other => {
87- tracing:: warn!(
88- "Unknown OTEL_EXPORTER_OTLP_PROTOCOL value '{}'; disabling OTEL tracing. \
89- Supported values: grpc, http/protobuf, http/json",
90- other
91- ) ;
92- return None ;
81+ let processor = if use_stdout {
82+ let exporter = opentelemetry_stdout:: SpanExporter :: default ( ) ;
83+ BatchSpanProcessor :: builder ( exporter) . build ( )
84+ } else {
85+ match protocol. as_str ( ) {
86+ "grpc" => {
87+ let exporter = opentelemetry_otlp:: SpanExporter :: builder ( )
88+ . with_tonic ( )
89+ . build ( )
90+ . map_err ( |e| tracing:: warn!( "Failed to build OTEL span exporter: {}" , e) )
91+ . ok ( ) ?;
92+ BatchSpanProcessor :: builder ( exporter) . build ( )
93+ }
94+ "http/protobuf" => {
95+ let exporter = SpanExporter :: builder ( )
96+ . with_http ( )
97+ . with_protocol ( Protocol :: HttpBinary )
98+ . build ( )
99+ . map_err ( |e| tracing:: warn!( "Failed to build OTEL span exporter: {}" , e) )
100+ . ok ( ) ?;
101+ BatchSpanProcessor :: builder ( exporter) . build ( )
102+ }
103+ "http/json" => {
104+ let exporter = SpanExporter :: builder ( )
105+ . with_http ( )
106+ . with_protocol ( Protocol :: HttpJson )
107+ . build ( )
108+ . map_err ( |e| tracing:: warn!( "Failed to build OTEL span exporter: {}" , e) )
109+ . ok ( ) ?;
110+ BatchSpanProcessor :: builder ( exporter) . build ( )
111+ }
112+ other => {
113+ tracing:: warn!(
114+ "Unknown OTEL_EXPORTER_OTLP_PROTOCOL value '{}'; disabling OTEL tracing. \
115+ Supported values: grpc, http/protobuf, http/json",
116+ other
117+ ) ;
118+ return None ;
119+ }
93120 }
94121 } ;
95122
96- let exporter = exporter
97- . map_err ( |e| tracing:: warn!( "Failed to build OTEL span exporter: {}" , e) )
98- . ok ( ) ?;
99-
100123 // Declare conformance to OTel Semantic Conventions v1.56.0 via schema_url.
101124 // Downstream collectors (e.g., the Schema Translate Processor) can apply
102125 // migration tables to rewrite attribute names across semconv versions —
@@ -109,8 +132,6 @@ pub fn init_tracing(service: &'static str) -> Option<SdkTracerProvider> {
109132 )
110133 . build ( ) ;
111134
112- let processor = BatchSpanProcessor :: builder ( exporter) . build ( ) ;
113-
114135 let provider = SdkTracerProvider :: builder ( )
115136 . with_span_processor ( processor)
116137 . with_resource ( resource)
0 commit comments