Skip to main content

Overview

The opentelemetry-appender-log crate provides a bridge between the log crate and OpenTelemetry. It implements the log::Log trait to capture logs emitted through the log crate’s macros (error!, warn!, info!, debug!, trace!) and forwards them to OpenTelemetry exporters.

Installation

Add the following to your Cargo.toml:
[dependencies]
log = "0.4"
opentelemetry = { version = "0.27", features = ["logs"] }
opentelemetry-appender-log = "0.27"
opentelemetry-sdk = { version = "0.27", features = ["logs"] }
opentelemetry-stdout = { version = "0.27", features = ["logs"] }

Quick Start

1

Create a LoggerProvider

Set up the OpenTelemetry logger provider with an exporter:
use opentelemetry_sdk::logs::SdkLoggerProvider;
use opentelemetry_stdout::LogExporter;

let exporter = LogExporter::default();
let provider = SdkLoggerProvider::builder()
    .with_simple_exporter(exporter)
    .build();
2

Install the log appender

Create and register the OpenTelemetry log bridge:
use opentelemetry_appender_log::OpenTelemetryLogBridge;
use log::Level;

let appender = OpenTelemetryLogBridge::new(&provider);
log::set_boxed_logger(Box::new(appender)).unwrap();
log::set_max_level(Level::Info.to_level_filter());
3

Emit logs

Use standard log macros:
use log::{error, warn, info};

error!("Application error occurred");
warn!("Warning message");
info!("Application started successfully");
4

Shutdown

Flush remaining logs before exit:
provider.shutdown().unwrap();

Complete Example

use log::{error, info, warn, Level};
use opentelemetry_appender_log::OpenTelemetryLogBridge;
use opentelemetry_sdk::{logs::SdkLoggerProvider, Resource};
use opentelemetry_stdout::LogExporter;

#[tokio::main]
async fn main() {
    // Create an exporter that writes to stdout
    let exporter = LogExporter::default();

    // Create a LoggerProvider with resource attributes
    let provider = SdkLoggerProvider::builder()
        .with_resource(
            Resource::builder()
                .with_service_name("my-service")
                .build(),
        )
        .with_simple_exporter(exporter)
        .build();

    // Set up the log appender
    let appender = OpenTelemetryLogBridge::new(&provider);
    log::set_boxed_logger(Box::new(appender)).unwrap();
    log::set_max_level(Level::Info.to_level_filter());

    // Emit logs with structured key-values
    let fruit = "apple";
    let price = 2.99;

    error!(fruit, price; "hello from {fruit}. My price is {price}");
    warn!("Warning message");
    info!("Application started");

    // Shutdown to ensure all logs are flushed
    provider.shutdown().unwrap();
}

Field Mapping

The appender maps log::Record fields to OpenTelemetry LogRecord fields:

Basic Fields

log FieldOpenTelemetry FieldNotes
args()bodyThe formatted log message
level()severity_numberMapped using severity table below
level()severity_textString representation (“ERROR”, “INFO”)
target()targetModule/component identifier
key_values()attributesStructured key-value pairs

Severity Mapping

log::LevelSeverity TextSeverity Number
ErrorERROR17
WarnWARN13
InfoINFO9
DebugDEBUG5
TraceTRACE1

Metadata Attributes (Experimental)

With the experimental_metadata_attributes feature, source code metadata is captured:
[dependencies]
opentelemetry-appender-log = { version = "0.27", features = ["experimental_metadata_attributes"] }
This adds attributes:
log FieldAttribute KeyExample Value
file()code.filepathsrc/main.rs
line()code.lineno42
module_path()code.namespacemy_app::module

Key-Value Attributes

The log crate supports structured logging with key-value pairs:
use log::info;

info!(
    user_id = 12345,
    action = "login",
    duration_ms = 150;
    "User logged in successfully"
);
These are converted to OpenTelemetry attributes based on their type:

Type Mapping

Rust TypeAnyValue TypeNotes
i8-i64Int
u8-u64IntConverted to i64 if possible, else String
i128, u128Int or StringUses Int if fits in i64, else stringified
f32, f64Double
boolBoolean
&str, StringString
Other typesStringFormatted using Debug

With Serde Support

Enable the with-serde feature for complex types:
[dependencies]
opentelemetry-appender-log = { version = "0.27", features = ["with-serde"] }
With this feature enabled: | Type | Result | Notes | |-------------------|---------------|------------------------------------|| | Sequences | ListAny | Vectors, arrays, etc. | | Maps | Map | HashMaps, BTreeMaps, etc. | | Structs | Map | Serialized as key-value maps | | Enums | Various | Depends on variant type | | Bytes | Bytes | Raw byte arrays | | Option::None | — | Discarded | | Option::Some(T) | T | Uses inner value | | () | — | Discarded | Example:
use log::info;
use serde::Serialize;

#[derive(Serialize)]
struct UserInfo {
    id: u64,
    name: String,
    roles: Vec<String>,
}

let user = UserInfo {
    id: 12345,
    name: "alice".to_string(),
    roles: vec!["admin".to_string(), "user".to_string()],
};

info!(user = log::kv::Value::from_serde(&user); "User loaded");
Without with-serde, complex types are formatted using Debug:
// Without with-serde feature
info!(data = vec![1, 2, 3]; "Processing data");
// Attribute value: "[1, 2, 3]" (string)

Usage with Batch Processor

For production use, configure a batch processor:
use opentelemetry_sdk::logs::BatchLogProcessor;
use std::time::Duration;

let exporter = opentelemetry_stdout::LogExporter::default();

let processor = BatchLogProcessor::builder(exporter)
    .with_batch_config(
        opentelemetry_sdk::logs::BatchConfigBuilder::default()
            .with_max_queue_size(2048)
            .with_max_export_batch_size(512)
            .with_scheduled_delay(Duration::from_secs(5))
            .build(),
    )
    .build();

let provider = SdkLoggerProvider::builder()
    .with_log_processor(processor)
    .build();

Integration with Traces

When logs are emitted within an active OpenTelemetry span context, the trace context is automatically attached:
use opentelemetry::trace::{Tracer, TracerProvider};
use opentelemetry_sdk::trace::SdkTracerProvider;
use log::error;

let tracer_provider = SdkTracerProvider::builder().build();
let tracer = tracer_provider.tracer("my-app");

tracer.in_span("process-request", |_cx| {
    // This log will have trace_id and span_id from the active span
    error!("Request processing failed");
});
The emitted log will include:
  • trace_id: The ID of the distributed trace
  • span_id: The ID of the current span
  • trace_flags: Sampling flags

Performance Considerations

Filtering

Set appropriate log levels to avoid overhead:
// Only process Info and above
log::set_max_level(log::Level::Info.to_level_filter());

event_enabled

The appender implements enabled() to check if a log should be processed:
// This check happens before expensive formatting
if log::log_enabled!(log::Level::Debug) {
    let expensive_data = compute_expensive_data();
    log::debug!(data = expensive_data; "Debug information");
}

Batching

Use BatchLogProcessor in production to amortize export costs:
  • Reduces network overhead
  • Improves throughput
  • Adds minimal latency (configurable)
See Log Processors for configuration details.

Comparison with tracing Appender

Featurelog Appendertracing Appender
Target Cratelogtracing
Structured LoggingKey-valuesFields + spans
Span ContextManualAutomatic
Async-awareNoYes
FilteringLevel-basedTarget + level-based
Event NamesNot supportedSupported
Best ForSimple applicationsAsync applications
Choose the log appender if:
  • You have existing code using the log crate
  • You need simple, synchronous logging
  • You don’t need advanced filtering or span correlation
Choose the tracing appender if:
  • You’re building async applications
  • You want hierarchical span context
  • You need advanced filtering capabilities
  • You want automatic trace correlation

Feature Flags

FeatureDescription
with-serdeSupport complex types via serde serialization
experimental_metadata_attributesCapture source code location as attributes

Troubleshooting

Logs not appearing

  1. Check log level: Ensure set_max_level() is set appropriately
    log::set_max_level(log::LevelFilter::Debug);
    
  2. Flush on shutdown: Always call provider.shutdown()
    provider.shutdown().unwrap();
    
  3. Check processor configuration: Verify the exporter is configured correctly

Complex types not serialized

Enable the with-serde feature:
opentelemetry-appender-log = { version = "0.27", features = ["with-serde"] }

Trace context not attached

Ensure logs are emitted within an active span context:
tracer.in_span("my-operation", |_cx| {
    log::info!("This log will have trace context");
});

See Also

tracing Appender

Alternative appender for the tracing 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