The stdout exporter writes telemetry data to standard output, making it ideal for development, debugging, and learning. It supports all three OpenTelemetry signals: traces, metrics, and logs.
The stdout exporter is designed for development and debugging only . It is not optimized for production use, and the output format may change between versions.
Installation
Add the dependency to your Cargo.toml:
[ dependencies ]
opentelemetry = "0.31"
opentelemetry_sdk = "0.31"
opentelemetry-stdout = "0.31"
Feature Flags
The stdout exporter supports selective signal features:
# Default (all signals)
opentelemetry-stdout = "0.31"
# Traces only
opentelemetry-stdout = { version = "0.31" , features = [ "trace" ], default-features = false }
# Metrics only
opentelemetry-stdout = { version = "0.31" , features = [ "metrics" ], default-features = false }
# Logs only
opentelemetry-stdout = { version = "0.31" , features = [ "logs" ], default-features = false }
Quick Start
Traces
use opentelemetry :: global;
use opentelemetry :: trace :: Tracer ;
use opentelemetry_sdk :: trace :: SdkTracerProvider ;
use opentelemetry_sdk :: Resource ;
use opentelemetry_stdout :: SpanExporter ;
fn main () -> Result <(), Box < dyn std :: error :: Error >> {
// Create stdout exporter for traces
let exporter = SpanExporter :: default ();
// Create tracer provider
let provider = SdkTracerProvider :: builder ()
. with_simple_exporter ( exporter )
. with_resource (
Resource :: builder ()
. with_service_name ( "stdout-example" )
. build ()
)
. build ();
global :: set_tracer_provider ( provider . clone ());
// Create and use a tracer
let tracer = global :: tracer ( "my-tracer" );
tracer . in_span ( "example-operation" , | _cx | {
println! ( "Doing some work..." );
});
// Shutdown to flush spans
provider . shutdown () ? ;
Ok (())
}
Metrics
use opentelemetry :: global;
use opentelemetry :: KeyValue ;
use opentelemetry_sdk :: metrics :: SdkMeterProvider ;
use opentelemetry_sdk :: Resource ;
use opentelemetry_stdout :: MetricExporter ;
fn main () -> Result <(), Box < dyn std :: error :: Error >> {
// Create stdout exporter for metrics
let exporter = MetricExporter :: default ();
// Create meter provider
let provider = SdkMeterProvider :: builder ()
. with_periodic_exporter ( exporter )
. with_resource (
Resource :: builder ()
. with_service_name ( "stdout-example" )
. build ()
)
. build ();
global :: set_meter_provider ( provider . clone ());
// Create and use a meter
let meter = global :: meter ( "my-meter" );
let counter = meter . u64_counter ( "requests" ) . build ();
counter . add ( 1 , & [ KeyValue :: new ( "endpoint" , "/api/users" )]);
// Shutdown to flush metrics
provider . shutdown () ? ;
Ok (())
}
Logs
use opentelemetry_appender_tracing :: layer :: OpenTelemetryTracingBridge ;
use opentelemetry_sdk :: logs :: SdkLoggerProvider ;
use opentelemetry_sdk :: Resource ;
use opentelemetry_stdout :: LogExporter ;
use tracing :: info;
use tracing_subscriber :: prelude ::* ;
fn main () -> Result <(), Box < dyn std :: error :: Error >> {
// Create stdout exporter for logs
let exporter = LogExporter :: default ();
// Create logger provider
let provider = SdkLoggerProvider :: builder ()
. with_simple_exporter ( exporter )
. with_resource (
Resource :: builder ()
. with_service_name ( "stdout-example" )
. build ()
)
. build ();
// Bridge tracing to OpenTelemetry
let layer = OpenTelemetryTracingBridge :: new ( & provider );
tracing_subscriber :: registry () . with ( layer ) . init ();
// Use tracing macros
info! ( "Application started" );
info! ( user_id = "12345" , "User logged in" );
// Shutdown to flush logs
provider . shutdown () ? ;
Ok (())
}
Complete Example (All Signals)
Here’s a comprehensive example that demonstrates all three signals:
use once_cell :: sync :: Lazy ;
use opentelemetry :: {global, trace :: { TraceContextExt , Tracer }, InstrumentationScope , KeyValue };
use opentelemetry_appender_tracing :: layer :: OpenTelemetryTracingBridge ;
use opentelemetry_sdk :: {
logs :: SdkLoggerProvider ,
metrics :: SdkMeterProvider ,
trace :: SdkTracerProvider ,
Resource ,
};
use tracing :: info;
use tracing_subscriber :: prelude ::* ;
static RESOURCE : Lazy < Resource > = Lazy :: new ( || {
Resource :: builder ()
. with_service_name ( "demo-app" )
. with_service_version ( "1.0.0" )
. build ()
});
fn init_traces () -> SdkTracerProvider {
let exporter = opentelemetry_stdout :: SpanExporter :: default ();
let provider = SdkTracerProvider :: builder ()
. with_simple_exporter ( exporter )
. with_resource ( RESOURCE . clone ())
. build ();
global :: set_tracer_provider ( provider . clone ());
provider
}
fn init_metrics () -> SdkMeterProvider {
let exporter = opentelemetry_stdout :: MetricExporter :: default ();
let provider = SdkMeterProvider :: builder ()
. with_periodic_exporter ( exporter )
. with_resource ( RESOURCE . clone ())
. build ();
global :: set_meter_provider ( provider . clone ());
provider
}
fn init_logs () -> SdkLoggerProvider {
let exporter = opentelemetry_stdout :: LogExporter :: default ();
let provider = SdkLoggerProvider :: builder ()
. with_simple_exporter ( exporter )
. with_resource ( RESOURCE . clone ())
. build ();
let layer = OpenTelemetryTracingBridge :: new ( & provider );
tracing_subscriber :: registry () . with ( layer ) . init ();
provider
}
#[tokio :: main]
async fn main () -> Result <(), Box < dyn std :: error :: Error >> {
// Initialize all providers
let tracer_provider = init_traces ();
let meter_provider = init_metrics ();
let logger_provider = init_logs ();
// Create instrumentation scope
let scope = InstrumentationScope :: builder ( "demo-app" )
. with_version ( "1.0" )
. with_attributes ([ KeyValue :: new ( "environment" , "development" )])
. build ();
let tracer = global :: tracer_with_scope ( scope . clone ());
let meter = global :: meter_with_scope ( scope );
// Create metrics
let request_counter = meter
. u64_counter ( "http_requests_total" )
. with_description ( "Total HTTP requests" )
. build ();
let request_duration = meter
. f64_histogram ( "http_request_duration_seconds" )
. with_description ( "HTTP request duration" )
. build ();
// Record some metrics
request_counter . add ( 1 , & [
KeyValue :: new ( "method" , "GET" ),
KeyValue :: new ( "endpoint" , "/api/users" ),
]);
request_duration . record ( 0.123 , & [
KeyValue :: new ( "method" , "GET" ),
KeyValue :: new ( "status" , "200" ),
]);
// Create traces with logs
tracer . in_span ( "handle_request" , | cx | {
let span = cx . span ();
span . set_attribute ( KeyValue :: new ( "user.id" , "12345" ));
span . set_attribute ( KeyValue :: new ( "request.path" , "/api/users" ));
info! ( "Processing user request" );
span . add_event (
"Request validated" ,
vec! [ KeyValue :: new ( "validation.time_ms" , 5 )],
);
tracer . in_span ( "database_query" , | cx | {
let span = cx . span ();
span . set_attribute ( KeyValue :: new ( "db.system" , "postgresql" ));
info! ( "Querying database" );
});
info! ( "Request completed successfully" );
});
// Shutdown all providers
tracer_provider . shutdown () ? ;
meter_provider . shutdown () ? ;
logger_provider . shutdown () ? ;
Ok (())
}
The stdout exporter prints telemetry in a human-readable JSON-like format:
Trace Output Example
Span {
name : "handle_request" ,
trace_id : 5 b 8 aa 5 a 2 d 2 c 872e8137 a 28 db 9 d 321 b 50 ,
span_id : 5 fb 397 be 34 d 26 b 51 ,
parent_span_id : 0000000000000000 ,
start_time : 2026-03-02 T 12 : 00 : 00.123456 Z ,
end_time : 2026-03-02 T 12 : 00 : 00.234567 Z ,
attributes : {
"user.id" : "12345" ,
"request.path" : "/api/users"
},
events : [
Event {
name : "Request validated" ,
timestamp : 2026-03-02 T 12 : 00 : 00.150000 Z ,
attributes : { "validation.time_ms" : 5 }
}
],
status : Ok
}
Metric Output Example
Metric {
name : "http_requests_total" ,
description : "Total HTTP requests" ,
unit : "" ,
data : Sum {
value : 1 ,
attributes : {
"method" : "GET" ,
"endpoint" : "/api/users"
}
}
}
Log Output Example
LogRecord {
timestamp : 2026-03-02 T 12 : 00 : 00.123456 Z ,
severity : Info ,
body : "Processing user request" ,
attributes : {},
trace_id : 5 b 8 aa 5 a 2 d 2 c 872e8137 a 28 db 9 d 321 b 50 ,
span_id : 5 fb 397 be 34 d 26 b 51
}
Configuration
Custom Writer
By default, output goes to stdout. You can customize the destination:
use std :: io :: Write ;
use opentelemetry_stdout :: SpanExporter ;
// Example: Write to a file
let file = std :: fs :: File :: create ( "traces.log" ) ? ;
let exporter = SpanExporter :: new ( file );
Resource Configuration
Add resource attributes that appear in all telemetry:
use opentelemetry :: KeyValue ;
use opentelemetry_sdk :: Resource ;
let resource = Resource :: builder ()
. with_service_name ( "my-service" )
. with_service_version ( "1.2.3" )
. with_attributes ([
KeyValue :: new ( "environment" , "development" ),
KeyValue :: new ( "region" , "local" ),
KeyValue :: new ( "host.name" , "laptop" ),
])
. build ();
let provider = SdkTracerProvider :: builder ()
. with_simple_exporter ( exporter )
. with_resource ( resource )
. build ();
Use Cases
Local Development
Quickly verify instrumentation without setting up a backend:
let exporter = opentelemetry_stdout :: SpanExporter :: default ();
let provider = SdkTracerProvider :: builder ()
. with_simple_exporter ( exporter )
. build ();
Testing
Validate telemetry data in tests:
#[cfg(test)]
mod tests {
use super ::* ;
#[test]
fn test_instrumentation () {
let exporter = opentelemetry_stdout :: SpanExporter :: default ();
let provider = SdkTracerProvider :: builder ()
. with_simple_exporter ( exporter )
. build ();
// Your test code that generates spans
// Spans will be printed to stdout for inspection
}
}
Learning and Education
Understand the structure of OpenTelemetry data:
// Students can see exactly what data is being collected
let exporter = opentelemetry_stdout :: SpanExporter :: default ();
Debugging Production Issues Locally
Reproduce issues with realistic data:
// Switch to stdout exporter temporarily for debugging
#[cfg(debug_assertions)]
let exporter = opentelemetry_stdout :: SpanExporter :: default ();
#[cfg(not(debug_assertions))]
let exporter = opentelemetry_otlp :: SpanExporter :: builder ()
. with_http ()
. build () ? ;
The stdout exporter writes synchronously and can block your application. It is not suitable for production use.
Each telemetry item is written immediately
I/O operations block the calling thread
Large volumes of telemetry can slow down your application
No batching or buffering
Alternatives for Production
For production, use:
Piping to jq
Format output with jq for better readability:
Filtering Output
Filter specific telemetry:
# Only show errors
cargo run 2>&1 | grep -i error
# Only show spans
cargo run 2>&1 | grep "Span {"
Saving to File
Capture output for later analysis:
cargo run > telemetry.log 2>&1
Troubleshooting
No Output Appearing
Ensure shutdown is called:
provider . shutdown () ? ; // Flushes all pending data
Check feature flags:
opentelemetry-stdout = { version = "0.31" , features = [ "trace" ] }
Verify exporter is set:
let provider = SdkTracerProvider :: builder ()
. with_simple_exporter ( exporter ) // Must set exporter
. build ();
Output Mixed with Application Logs
Redirect stderr separately:
cargo run 2> telemetry.log 1> app.log
Or use a custom writer:
let file = std :: fs :: File :: create ( "telemetry.log" ) ? ;
let exporter = opentelemetry_stdout :: SpanExporter :: new ( file );
The stdout exporter is not optimized for high-volume telemetry. For production:
// Switch to OTLP for production
let exporter = opentelemetry_otlp :: SpanExporter :: builder ()
. with_http ()
. build () ? ;
Comparison with Other Exporters
Feature Stdout OTLP Zipkin Traces ✓ ✓ ✓ Metrics ✓ ✓ ✗ Logs ✓ ✓ ✗ Production Ready ✗ ✓ ✓ Batching ✗ ✓ ✓ Backend Required ✗ ✓ ✓ Use Case Development Production Zipkin backends
Next Steps
OTLP Exporter Production-ready exporter for all signals
Tracing Guide Learn how to instrument your application