Skip to main content

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

1

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();
2

Create the tracing bridge layer

Create the OpenTelemetryTracingBridge layer:
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;

let otel_layer = OpenTelemetryTracingBridge::new(&provider);
3

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();
4

Emit logs

Use standard tracing macros:
use tracing::error;

error!(
    name: "user-login-failed",
    target: "auth-service",
    user_id = 12345,
    message = "Invalid credentials"
);
5

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 FieldOpenTelemetry FieldNotes
event nameevent_nameEvents in tracing become OTel Events
targettargetGroups logs by module/crate; used as instrumentation scope
levelseverity_numberMapped using severity table below
levelseverity_textString representation (“ERROR”, “INFO”)
fieldsattributesConverted to key-value attributes
”message” fieldbodyIf 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::LevelSeverity TextSeverity Number
ERRORERROR17
WARNWARN13
INFOINFO9
DEBUGDEBUG5
TRACETRACE1

Type Mapping

tracing field types are mapped to AnyValue:
tracing TypeAnyValue TypeNotes
i64Int
u64Int or StringIf fits in i64, else stringified
u128, i128Int or StringIf fits in i64, else stringified
f32, f64Double
&strString
boolBoolean
&[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

Featuretracing Appenderlog Appender
Target Cratetracinglog
Structured LoggingFields + spansKey-values
Span ContextAutomaticManual
Async-awareYesNo
FilteringTarget + levelLevel only
Event NamesSupportedNot supported
Best ForAsync applicationsSimple 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

FeatureDescription
experimental_span_attributesCapture span fields as log attributes
experimental_metadata_attributesCapture source code location as attributes

Limitations

  1. No Valuable support: The appender does not currently support the valuable crate for efficient serialization. See issue #2819.
  2. 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

  1. Check that the layer is registered:
    tracing_subscriber::registry()
        .with(otel_layer)
        .init();
    
  2. Verify filtering isn’t too restrictive:
    let filter = EnvFilter::new("debug");
    
  3. 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

Build docs developers (and LLMs) love