Overview
The opentelemetry-appender-tracing crate bridges the tracing crate to OpenTelemetry logs. It provides a tracing::Layer implementation that converts tracing events into OpenTelemetry LogRecords, enabling seamless integration for async Rust applications.
Unlike traces and metrics, OpenTelemetry does not provide a dedicated logging API for end-users. Instead, it recommends using existing logging libraries like tracing and bridging them to OpenTelemetry logs.
Key Features
Integrates as a tracing-subscriber Layer alongside other layers (e.g., fmt)
Automatically attaches OpenTelemetry trace context (TraceId, SpanId, TraceFlags) to logs
Automatically associates OpenTelemetry Resource to logs
Supports exporting to OpenTelemetry-compatible backends (OTLP, stdout, etc.)
Optional: Capture span attributes in log records (experimental)
Installation
Add the following to your Cargo.toml:
[ dependencies ]
tracing = "0.1"
tracing-subscriber = { version = "0.3" , features = [ "registry" , "std" ] }
opentelemetry = { version = "0.27" , features = [ "logs" ] }
opentelemetry-sdk = { version = "0.27" , features = [ "logs" ] }
opentelemetry-appender-tracing = "0.27"
opentelemetry-stdout = { version = "0.27" , features = [ "logs" ] }
Quick Start
Create a LoggerProvider
Set up the OpenTelemetry logger provider: use opentelemetry_sdk :: logs :: SdkLoggerProvider ;
use opentelemetry_stdout :: LogExporter ;
let exporter = LogExporter :: default ();
let provider = SdkLoggerProvider :: builder ()
. with_simple_exporter ( exporter )
. build ();
Create the tracing bridge layer
Create the OpenTelemetryTracingBridge layer: use opentelemetry_appender_tracing :: layer :: OpenTelemetryTracingBridge ;
let otel_layer = OpenTelemetryTracingBridge :: new ( & provider );
Register with tracing subscriber
Combine with other layers and initialize: use tracing_subscriber :: prelude ::* ;
tracing_subscriber :: registry ()
. with ( otel_layer )
. with ( tracing_subscriber :: fmt :: layer ())
. init ();
Emit logs
Use standard tracing macros: use tracing :: error;
error! (
name : "user-login-failed" ,
target : "auth-service" ,
user_id = 12345 ,
message = "Invalid credentials"
);
Shutdown
Flush remaining logs: provider . shutdown () . unwrap ();
Complete Example
use opentelemetry_appender_tracing :: layer :: OpenTelemetryTracingBridge ;
use opentelemetry_sdk :: logs :: SdkLoggerProvider ;
use opentelemetry_sdk :: Resource ;
use tracing :: {error, info};
use tracing_subscriber :: { prelude ::* , EnvFilter };
fn main () {
// Create exporter and provider
let exporter = opentelemetry_stdout :: LogExporter :: default ();
let provider = SdkLoggerProvider :: builder ()
. with_resource (
Resource :: builder ()
. with_service_name ( "my-service" )
. build (),
)
. with_simple_exporter ( exporter )
. build ();
// Create OpenTelemetry layer with filtering
let filter_otel = EnvFilter :: new ( "info" )
. add_directive ( "hyper=off" . parse () . unwrap ())
. add_directive ( "tonic=off" . parse () . unwrap ());
let otel_layer = OpenTelemetryTracingBridge :: new ( & provider )
. with_filter ( filter_otel );
// Create fmt layer for console output
let fmt_layer = tracing_subscriber :: fmt :: layer ()
. with_thread_names ( true );
// Initialize subscriber with both layers
tracing_subscriber :: registry ()
. with ( otel_layer )
. with ( fmt_layer )
. init ();
// Emit logs
info! ( "Application started" );
error! (
name : "my-event-name" ,
target : "my-system" ,
event_id = 20 ,
user_name = "otel" ,
user_email = "[email protected] " ,
message = "Example error message"
);
// Shutdown
provider . shutdown () . unwrap ();
}
Field Mapping
The appender maps tracing::Event to OpenTelemetry LogRecord:
Basic Mapping
tracing Field OpenTelemetry Field Notes event name event_nameEvents in tracing become OTel Events target targetGroups logs by module/crate; used as instrumentation scope level severity_numberMapped using severity table below level severity_textString representation (“ERROR”, “INFO”) fields attributesConverted to key-value attributes ”message” field bodyIf present, used as log body
If a field named message exists, it’s used as the log body. Otherwise, the body is empty or derived from the event’s formatted message.
Severity Mapping
tracing::Level Severity Text Severity Number ERRORERROR 17 WARNWARN 13 INFOINFO 9 DEBUGDEBUG 5 TRACETRACE 1
Type Mapping
tracing field types are mapped to AnyValue:
tracing Type AnyValue Type Notes i64Intu64Int or StringIf fits in i64, else stringified u128, i128Int or StringIf fits in i64, else stringified f32, f64Double&strStringboolBoolean&[u8]Bytes&dyn DebugStringFormatted via Debug &dyn ErrorStringStored as exception.message attribute
Event Names and Targets
Event Names
You can specify event names explicitly:
use tracing :: error;
error! ( name : "user-login-failed" , user_id = 123 , "Login attempt failed" );
Without an explicit name, tracing generates a default based on source location:
error! ( user_id = 123 , "Login attempt failed" );
// Event name: "event src/main.rs:42" (example)
Targets
Targets group logs by module:
error! ( target : "auth-service" , "Authentication failed" );
Without an explicit target, tracing uses the module path:
// In module my_app::auth
error! ( "Authentication failed" );
// Target: "my_app::auth"
Exporters use the target field as the OpenTelemetry instrumentation scope name.
Trace Context Integration
When logs are emitted within an active OpenTelemetry span, the trace context is automatically attached:
use opentelemetry :: trace :: { Tracer , TracerProvider };
use opentelemetry_sdk :: trace :: SdkTracerProvider ;
use tracing :: error;
let tracer_provider = SdkTracerProvider :: builder () . build ();
let tracer = tracer_provider . tracer ( "my-app" );
tracer . in_span ( "process-request" , | _cx | {
// This log will include trace_id, span_id, and trace_flags
error! ( request_id = "req-123" , "Request processing failed" );
});
The log record will contain:
trace_id : Links the log to the distributed trace
span_id : Links the log to the specific span
trace_flags : Sampling information
Span Attributes (Experimental)
With the experimental_span_attributes feature, span fields are automatically captured as log attributes:
[ dependencies ]
opentelemetry-appender-tracing = { version = "0.27" , features = [ "experimental_span_attributes" ] }
Example:
use tracing :: {error, info_span};
let span = info_span! (
"process_user" ,
user_id = 12345 ,
session_id = "abc-def"
);
let _enter = span . enter ();
// This log will include user_id and session_id from the span
error! ( status = 500 , "User processing failed" );
The log attributes will include:
user_id = 12345 (from span)
session_id = "abc-def" (from span)
status = 500 (from event)
Nested Spans
Attributes from all parent spans are collected:
use tracing :: {error, info_span};
let outer = info_span! ( "request" , request_id = "req-123" );
let _outer_guard = outer . enter ();
let inner = info_span! ( "database" , query = "SELECT * FROM users" );
let _inner_guard = inner . enter ();
// Log includes both request_id and query
error! ( "Database query failed" );
Attribute Allowlist
Filter which span attributes to capture:
let layer = OpenTelemetryTracingBridge :: builder ( & provider )
. with_span_attribute_allowlist ([ "user_id" , "request_id" ])
. build ();
Only user_id and request_id span attributes will be captured in logs.
Filtering
By Level
Use tracing-subscriber filters:
use tracing_subscriber :: { EnvFilter , prelude ::* };
let filter = EnvFilter :: new ( "info" )
. add_directive ( "my_app=debug" . parse () . unwrap ());
let layer = OpenTelemetryTracingBridge :: new ( & provider )
. with_filter ( filter );
Suppress Telemetry Loops
When using OTLP exporters, prevent telemetry-induced-telemetry loops:
let filter = EnvFilter :: new ( "info" )
. add_directive ( "hyper=off" . parse () . unwrap ())
. add_directive ( "tonic=off" . parse () . unwrap ())
. add_directive ( "h2=off" . parse () . unwrap ())
. add_directive ( "reqwest=off" . parse () . unwrap ());
This filters out logs from HTTP/gRPC libraries used by the OTLP exporter.
This filtering also drops logs from these libraries when used outside of the exporter. For more targeted filtering, see the open issue .
Advanced Configuration
Builder Pattern
Use the builder for advanced configuration:
use opentelemetry_appender_tracing :: layer :: OpenTelemetryTracingBridge ;
let layer = OpenTelemetryTracingBridge :: builder ( & provider )
. with_span_attribute_allowlist ([ "user_id" , "session_id" ])
. build ();
Multiple Layers
Combine with other tracing layers:
use tracing_subscriber :: {fmt, prelude ::* , EnvFilter };
let otel_layer = OpenTelemetryTracingBridge :: new ( & provider );
let fmt_layer = fmt :: layer ()
. with_thread_names ( true )
. with_file ( true )
. with_line_number ( true );
tracing_subscriber :: registry ()
. with ( otel_layer )
. with ( fmt_layer )
. init ();
Batch Processing
For production, use batch processing:
use opentelemetry_sdk :: logs :: BatchLogProcessor ;
use std :: time :: Duration ;
let processor = BatchLogProcessor :: builder ( exporter )
. with_batch_config (
opentelemetry_sdk :: logs :: BatchConfigBuilder :: default ()
. with_max_queue_size ( 2048 )
. with_scheduled_delay ( Duration :: from_secs ( 5 ))
. build (),
)
. build ();
let provider = SdkLoggerProvider :: builder ()
. with_log_processor ( processor )
. build ();
Custom Log Processor
You can create custom processors to enrich logs:
use opentelemetry :: logs :: Severity ;
use opentelemetry :: InstrumentationScope ;
use opentelemetry_sdk :: error :: OTelSdkResult ;
use opentelemetry_sdk :: logs :: { LogProcessor , SdkLogRecord };
use opentelemetry_sdk :: Resource ;
#[derive( Debug )]
struct EnrichmentLogProcessor < P : LogProcessor > {
delegate : P ,
}
impl < P : LogProcessor > LogProcessor for EnrichmentLogProcessor < P > {
fn emit ( & self , record : & mut SdkLogRecord , scope : & InstrumentationScope ) {
// Add custom attribute
record . add_attribute ( "environment" , "production" );
// Forward to delegate
self . delegate . emit ( record , scope );
}
fn force_flush ( & self ) -> OTelSdkResult {
self . delegate . force_flush ()
}
fn shutdown_with_timeout ( & self , timeout : std :: time :: Duration ) -> OTelSdkResult {
self . delegate . shutdown_with_timeout ( timeout )
}
fn event_enabled ( & self , level : Severity , target : & str , name : Option < & str >) -> bool {
self . delegate . event_enabled ( level , target , name )
}
fn set_resource ( & mut self , resource : & Resource ) {
self . delegate . set_resource ( resource );
}
}
Use it in the provider:
use opentelemetry_sdk :: logs :: SimpleLogProcessor ;
let simple = SimpleLogProcessor :: new ( exporter );
let enriching = EnrichmentLogProcessor { delegate : simple };
let provider = SdkLoggerProvider :: builder ()
. with_log_processor ( enriching )
. build ();
Comparison with log Appender
Feature tracing Appender log Appender Target Crate tracinglogStructured Logging Fields + spans Key-values Span Context Automatic Manual Async-aware Yes No Filtering Target + level Level only Event Names Supported Not supported Best For Async applications Simple applications
Choose the tracing appender if:
You’re building async applications (tokio, async-std)
You want hierarchical span context
You need advanced filtering capabilities
You want automatic trace correlation
Choose the log appender if:
You have existing code using the log crate
You need simple, synchronous logging
You don’t need span correlation
Feature Flags
Feature Description experimental_span_attributesCapture span fields as log attributes experimental_metadata_attributesCapture source code location as attributes
Limitations
No Valuable support : The appender does not currently support the valuable crate for efficient serialization. See issue #2819 .
tracing-opentelemetry : This appender converts tracing events into logs, not spans. For converting tracing spans to OpenTelemetry spans, use the third-party tracing-opentelemetry crate.
Troubleshooting
Logs not appearing
Check that the layer is registered:
tracing_subscriber :: registry ()
. with ( otel_layer )
. init ();
Verify filtering isn’t too restrictive:
let filter = EnvFilter :: new ( "debug" );
Always shutdown the provider:
provider . shutdown () . unwrap ();
Trace context not attached
Ensure you’re using OpenTelemetry’s tracing, not just tracing:
use opentelemetry :: trace :: { Tracer , TracerProvider };
let tracer = tracer_provider . tracer ( "my-app" );
tracer . in_span ( "operation" , | _cx | {
tracing :: info! ( "This log will have trace context" );
});
Span attributes not captured
Enable the feature flag:
opentelemetry-appender-tracing = { version = "0.27" , features = [ "experimental_span_attributes" ] }
See Also
log Appender Alternative appender for the log crate
Log Processors Configure batch and simple processors
Bridge API Understanding the underlying API
Overview Return to logs overview