Skip to main content

Overview

The OpenTelemetry Logs Bridge API provides the foundation for connecting existing logging libraries to OpenTelemetry. This API is not intended for direct use by application developers—instead, it’s designed for logging library authors to build log appenders.
Application developers should use logging libraries like log or tracing along with the provided appenders, not the Bridge API directly.

Core Traits

The Bridge API defines three main traits in the opentelemetry::logs module:

Logger

The Logger trait provides methods to create and emit log records:
pub trait Logger {
    type LogRecord: LogRecord;

    /// Creates a new log record builder
    fn create_log_record(&self) -> Self::LogRecord;

    /// Emits a log record
    fn emit(&self, record: Self::LogRecord);

    /// Checks if a log event is enabled
    fn event_enabled(&self, level: Severity, target: &str, name: Option<&str>) -> bool;
}
Key methods:
  • create_log_record() - Creates a new log record that can be populated with data
  • emit() - Sends the log record to the logging pipeline
  • event_enabled() - Allows early filtering to skip expensive logging operations
When emit() is called within an active trace context, the logger automatically attaches the current trace ID, span ID, and trace flags to the log record.

LoggerProvider

The LoggerProvider trait creates Logger instances:
pub trait LoggerProvider {
    type Logger: Logger;

    /// Returns a logger with the given instrumentation scope
    fn logger_with_scope(&self, scope: InstrumentationScope) -> Self::Logger;

    /// Returns a logger with the given name
    fn logger(&self, name: impl Into<Cow<'static, str>>) -> Self::Logger {
        let scope = InstrumentationScope::builder(name).build();
        self.logger_with_scope(scope)
    }
}
Usage example:
use opentelemetry::logs::LoggerProvider;
use opentelemetry::InstrumentationScope;
use opentelemetry_sdk::logs::SdkLoggerProvider;

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

// Simple logger for applications
let logger = provider.logger("my-app");

// Logger with full instrumentation scope for libraries
let scope = InstrumentationScope::builder(env!("CARGO_PKG_NAME"))
    .with_version(env!("CARGO_PKG_VERSION"))
    .with_schema_url("https://opentelemetry.io/schemas/1.0.0")
    .build();
let logger = provider.logger_with_scope(scope);

LogRecord

The LogRecord trait provides methods to populate log record fields:
pub trait LogRecord {
    fn set_event_name(&mut self, name: &'static str);
    fn set_target<T: Into<Cow<'static, str>>>(&mut self, target: T);
    fn set_timestamp(&mut self, timestamp: SystemTime);
    fn set_observed_timestamp(&mut self, timestamp: SystemTime);
    fn set_severity_text(&mut self, text: &'static str);
    fn set_severity_number(&mut self, number: Severity);
    fn set_body(&mut self, body: AnyValue);
    fn add_attribute<K, V>(&mut self, key: K, value: V)
    where
        K: Into<Key>,
        V: Into<AnyValue>;
    fn add_attributes<I, K, V>(&mut self, attributes: I)
    where
        I: IntoIterator<Item = (K, V)>,
        K: Into<Key>,
        V: Into<AnyValue>;
    fn set_trace_context(
        &mut self,
        trace_id: TraceId,
        span_id: SpanId,
        trace_flags: Option<TraceFlags>,
    );
}

Core Types

Severity

The Severity enum defines 24 log severity levels:
pub enum Severity {
    Trace = 1,   Trace2 = 2,  Trace3 = 3,  Trace4 = 4,
    Debug = 5,   Debug2 = 6,  Debug3 = 7,  Debug4 = 8,
    Info = 9,    Info2 = 10,  Info3 = 11,  Info4 = 12,
    Warn = 13,   Warn2 = 14,  Warn3 = 15,  Warn4 = 16,
    Error = 17,  Error2 = 18, Error3 = 19, Error4 = 20,
    Fatal = 21,  Fatal2 = 22, Fatal3 = 23, Fatal4 = 24,
}
Most appenders map standard log levels to the base severity:
const fn severity_of_level(level: Level) -> Severity {
    match level {
        Level::Error => Severity::Error,  // 17
        Level::Warn => Severity::Warn,    // 13
        Level::Info => Severity::Info,    // 9
        Level::Debug => Severity::Debug,  // 5
        Level::Trace => Severity::Trace,  // 1
    }
}
Get the severity name:
let severity = Severity::Error;
assert_eq!(severity.name(), "ERROR");

AnyValue

The AnyValue enum represents values that can be stored in log attributes or body:
#[non_exhaustive]
pub enum AnyValue {
    Int(i64),
    Double(f64),
    String(StringValue),
    Boolean(bool),
    Bytes(Box<Vec<u8>>),
    ListAny(Box<Vec<AnyValue>>),
    Map(Box<HashMap<Key, AnyValue>>),
}
Automatic conversions:
use opentelemetry::logs::AnyValue;

// Integers (i8-i64, u8-u32)
let value: AnyValue = 42i32.into();
assert!(matches!(value, AnyValue::Int(42)));

// Floating point
let value: AnyValue = 3.14f64.into();
assert!(matches!(value, AnyValue::Double(_)));

// Strings
let value: AnyValue = "hello".into();
assert!(matches!(value, AnyValue::String(_)));

// Booleans
let value: AnyValue = true.into();
assert!(matches!(value, AnyValue::Boolean(true)));

// Bytes
let value: AnyValue = AnyValue::from(&b"data"[..]);
assert!(matches!(value, AnyValue::Bytes(_)));
Collections:
use std::collections::HashMap;
use opentelemetry::{logs::AnyValue, Key};

// Lists from iterators
let list: AnyValue = vec![1, 2, 3]
    .into_iter()
    .collect();
assert!(matches!(list, AnyValue::ListAny(_)));

// Maps from iterators
let mut map = HashMap::new();
map.insert("key", "value");
let map_value: AnyValue = map.into_iter().collect();
assert!(matches!(map_value, AnyValue::Map(_)));
Performance consideration: The log and tracing crates only support basic types (i64, f64, strings, bool). Complex types like ListAny and Map are available for custom appenders but involve heap allocations.

SDK Implementation

The opentelemetry-sdk crate provides the concrete implementation:
use opentelemetry_sdk::logs::SdkLoggerProvider;

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

let logger = provider.logger("my-logger");

SdkLogRecord

The SDK’s SdkLogRecord implements the LogRecord trait and stores:
  • Timestamps (event time and observed time)
  • Severity (number and text)
  • Body (the log message)
  • Attributes (structured key-value data)
  • Trace context (trace ID, span ID, flags)
  • Target (module/component identifier)
  • Event name (optional event identifier)

Building a Log Appender

Here’s how to build a custom log appender using the Bridge API:
1

Get a Logger

Obtain a logger from the provider:
let logger = provider.logger("");
Note: Log appenders typically use an empty scope name. See the semantic conventions issue.
2

Create a LogRecord

When a log event occurs:
let mut log_record = logger.create_log_record();
3

Populate the LogRecord

Map fields from the logging library to the log record:
log_record.set_severity_number(severity);
log_record.set_severity_text("ERROR");
log_record.set_body(message.into());
log_record.set_target(target);
log_record.add_attribute("key", "value");
4

Emit the LogRecord

Send the log record to the pipeline:
logger.emit(log_record);

Example: Simple Appender

use opentelemetry::logs::{AnyValue, Logger, LoggerProvider, Severity};

struct SimpleAppender<L> {
    logger: L,
}

impl<P, L> SimpleAppender<L>
where
    P: LoggerProvider<Logger = L>,
    L: Logger,
{
    fn new(provider: &P) -> Self {
        Self {
            logger: provider.logger(""),
        }
    }

    fn log(&self, level: Severity, message: &str) {
        // Check if the event is enabled before doing expensive work
        if !self.logger.event_enabled(level, "my-app", None) {
            return;
        }

        let mut record = self.logger.create_log_record();
        record.set_severity_number(level);
        record.set_severity_text(level.name());
        record.set_body(AnyValue::from(message.to_string()));
        record.set_target("my-app");

        self.logger.emit(record);
    }
}

Performance Optimization

event_enabled Check

Always check event_enabled() before expensive operations:
if !logger.event_enabled(severity, target, Some(name)) {
    return; // Skip expensive serialization
}

// Only do expensive work if the event is enabled
let mut record = logger.create_log_record();
// ... populate record ...
logger.emit(record);
This allows processors to filter logs early, avoiding unnecessary work for logs that would be dropped anyway.

Target Field

The target field serves a special purpose:
record.set_target("my_module::component");
  • Used for filtering and routing logs
  • Exporters may use this as the instrumentation scope name
  • Both log and tracing appenders default to the module path

Noop Implementation

For testing or when logging is disabled, use the noop provider:
use opentelemetry::logs::NoopLoggerProvider;

let provider = NoopLoggerProvider::new();
let logger = provider.logger("my-app");

// All operations are no-ops
let mut record = logger.create_log_record();
logger.emit(record); // Does nothing

See Also

log Appender

Implementation using the log crate

tracing Appender

Implementation using the tracing crate

Log Processors

Processing and exporting log records

Overview

Return to logs overview

Build docs developers (and LLMs) love