Skip to main content

Overview

OpenTelemetry logs provide structured logging capabilities that integrate seamlessly with traces and metrics. This guide shows how to use the tracing crate with OpenTelemetry’s log appender bridge.

Dependencies

Add these dependencies to your Cargo.toml:
Cargo.toml
[dependencies]
opentelemetry_sdk = { version = "*", features = ["logs"] }
opentelemetry-stdout = { version = "*", features = ["logs"] }
opentelemetry-appender-tracing = "*"
tracing = { version = "0.1", features = ["std"] }
tracing-subscriber = { version = "0.3", features = ["env-filter", "registry", "std", "fmt"] }

Complete Example

1

Initialize the Logger Provider

Set up the logger provider with a stdout exporter and resource attributes.
use opentelemetry_appender_tracing::layer;
use opentelemetry_sdk::logs::SdkLoggerProvider;
use opentelemetry_sdk::Resource;
use tracing_subscriber::{prelude::*, EnvFilter};

fn main() {
    let exporter = opentelemetry_stdout::LogExporter::default();
    let provider: SdkLoggerProvider = SdkLoggerProvider::builder()
        .with_resource(
            Resource::builder()
                .with_service_name("log-appender-tracing-example")
                .build(),
        )
        .with_simple_exporter(exporter)
        .build();
}
2

Configure Tracing Subscriber

Set up the tracing subscriber with OpenTelemetry layer and filtering.
// To prevent a telemetry-induced-telemetry loop, OpenTelemetry's own internal
// logging is properly suppressed. However, logs emitted by external components
// (such as reqwest, tonic, etc.) are not suppressed.
//
// The filter levels are set as follows:
// - Allow `info` level and above by default.
// - Completely restrict logs from `hyper`, `tonic`, `h2`, and `reqwest`.
let filter_otel = 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());

let otel_layer = layer::OpenTelemetryTracingBridge::new(&provider)
    .with_filter(filter_otel);
3

Add Console Output Layer

Optionally add a fmt layer for console output alongside OpenTelemetry.
// Create a new tracing::Fmt layer to print the logs to stdout
let filter_fmt = EnvFilter::new("info")
    .add_directive("opentelemetry=debug".parse().unwrap());

let fmt_layer = tracing_subscriber::fmt::layer()
    .with_thread_names(true)
    .with_filter(filter_fmt);

tracing_subscriber::registry()
    .with(otel_layer)
    .with(fmt_layer)
    .init();
4

Emit Log Records

Use the tracing macros to emit structured log records.
use tracing::error;

error!(
    name: "my-event-name",
    target: "my-system",
    event_id = 20,
    user_name = "otel",
    user_email = "[email protected]",
    message = "This is an example message"
);

// Don't forget to shutdown the provider
let _ = provider.shutdown();

Log Levels

The tracing crate provides standard log levels:
use tracing::{trace, debug, info, warn, error};

trace!("Very detailed diagnostic information");
debug!("Debug information");
info!("General informational messages");
warn!("Warning messages");
error!("Error messages");

Structured Fields

Add structured fields to your logs for better searchability:
use tracing::info;

info!(
    user_id = 12345,
    action = "login",
    ip_address = "192.168.1.1",
    "User logged in successfully"
);
The OpenTelemetry logs API is designed to be used as a bridge. The recommended approach is to use the tracing crate for logging in your application and let the OpenTelemetry appender bridge handle the export.

Filter Configuration

Use EnvFilter to control which logs are processed. This is crucial for preventing telemetry loops and reducing noise from third-party libraries.

Common Filter Patterns

// Allow all logs at info level or above
EnvFilter::new("info")

// Allow debug logs from your crate, info from others
EnvFilter::new("myapp=debug,info")

// Suppress specific crates entirely
EnvFilter::new("info")
    .add_directive("hyper=off".parse().unwrap())
    .add_directive("tonic=off".parse().unwrap())

Advanced: Custom Log Processors

You can create custom log processors to enrich or filter logs. Here’s a simple example:
use opentelemetry_sdk::logs::{LogProcessor, SdkLogRecord};
use opentelemetry::InstrumentationScope;
use opentelemetry_sdk::error::OTelSdkResult;
use std::time::Duration;

#[derive(Debug)]
struct EnrichmentLogProcessor<P: LogProcessor> {
    delegate: P,
}

impl<P: LogProcessor> LogProcessor for EnrichmentLogProcessor<P> {
    fn emit(&self, data: &mut SdkLogRecord, instrumentation: &InstrumentationScope) {
        // Add custom attributes
        data.add_attribute("enriched", true);
        self.delegate.emit(data, instrumentation);
    }

    fn force_flush(&self) -> OTelSdkResult {
        self.delegate.force_flush()
    }

    fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult {
        self.delegate.shutdown_with_timeout(timeout)
    }
}
For a complete implementation, see the logs-advanced example in the source repository.

Output Format

The stdout exporter will output logs in JSON format:
{
  "resourceLogs": [
    {
      "resource": {
        "attributes": [
          {
            "key": "service.name",
            "value": { "stringValue": "log-appender-tracing-example" }
          }
        ]
      },
      "scopeLogs": [
        {
          "logRecords": [
            {
              "severityText": "ERROR",
              "body": { "stringValue": "This is an example message" },
              "attributes": [
                { "key": "event_id", "value": { "intValue": 20 } },
                { "key": "user_name", "value": { "stringValue": "otel" } }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Build docs developers (and LLMs) love