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:
[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
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();
}
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);
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();
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.
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" } }
]
}
]
}
]
}
]
}