Skip to content

Commit 65969fd

Browse files
docs: logs enrichment (#3266)
1 parent 627f252 commit 65969fd

File tree

4 files changed

+160
-1
lines changed

4 files changed

+160
-1
lines changed

examples/README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This directory contains some examples that should help you get start crates from `opentelemetry-rust`.
44

5-
## log-basic
5+
## logs-basic
66

77
This example uses following crates from this repo:
88

@@ -12,6 +12,17 @@ This example uses following crates from this repo:
1212

1313
Check this example if you want to understand *how to instrument logs using opentelemetry*.
1414

15+
## logs-advanced
16+
17+
This example uses following crates from this repo:
18+
19+
- opentelemetry(log)
20+
- opentelemetry-appender-tracing
21+
- opentelemetry-stdout
22+
23+
This builds on top of the logs-basic,
24+
and shows how to implement and compose `LogProcessor`s..
25+
1526
## metrics-basic
1627

1728
This example uses following crates from this repo:

examples/logs-advanced/Cargo.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "logs-advanced"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "Apache-2.0"
6+
rust-version = "1.75.0"
7+
publish = false
8+
autobenches = false
9+
10+
[[bin]]
11+
name = "logs-advanced"
12+
path = "src/main.rs"
13+
bench = false
14+
15+
[dependencies]
16+
opentelemetry_sdk = { workspace = true }
17+
opentelemetry-stdout = { workspace = true, features = ["logs"] }
18+
opentelemetry-appender-tracing = { workspace = true }
19+
tracing = { workspace = true, features = ["std"]}
20+
tracing-subscriber = { workspace = true, features = ["env-filter","registry", "std", "fmt"] }
21+
opentelemetry = { workspace = true }

examples/logs-advanced/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# OpenTelemetry Log Processor Implementation and Composition - Example
2+
3+
This example builds on top of the `logs-basic`, showing how to implement `LogProcessor`s correctly.
4+
5+
The `EnrichmentProcessor` simulates a processor adding information
6+
to the log captured by the OpenTelemetry SDK, which correctly ensures that the
7+
downstream processor's filtering is captured, avoiding unnecessary work.
8+
9+
## Usage
10+
11+
```shell
12+
cargo run
13+
```

examples/logs-advanced/src/main.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use opentelemetry::logs::{LogRecord, Severity};
2+
use opentelemetry::InstrumentationScope;
3+
use opentelemetry_appender_tracing::layer;
4+
use opentelemetry_sdk::error::OTelSdkResult;
5+
use opentelemetry_sdk::logs::{LogProcessor, SdkLogRecord, SdkLoggerProvider, SimpleLogProcessor};
6+
use opentelemetry_sdk::Resource;
7+
use tracing::{error, info};
8+
use tracing_subscriber::{prelude::*, EnvFilter};
9+
10+
fn main() {
11+
let exporter = opentelemetry_stdout::LogExporter::default();
12+
let enriching_processor = EnrichmentLogProcessor::new(SimpleLogProcessor::new(exporter));
13+
let provider: SdkLoggerProvider = SdkLoggerProvider::builder()
14+
.with_resource(
15+
Resource::builder()
16+
.with_service_name("log-appender-tracing-example")
17+
.build(),
18+
)
19+
.with_log_processor(enriching_processor)
20+
.build();
21+
22+
// To prevent a telemetry-induced-telemetry loop, OpenTelemetry's own internal
23+
// logging is properly suppressed. However, logs emitted by external components
24+
// (such as reqwest, tonic, etc.) are not suppressed as they do not propagate
25+
// OpenTelemetry context. Until this issue is addressed
26+
// (https://github.com/open-telemetry/opentelemetry-rust/issues/2877),
27+
// filtering like this is the best way to suppress such logs.
28+
//
29+
// The filter levels are set as follows:
30+
// - Allow `info` level and above by default.
31+
// - Completely restrict logs from `hyper`, `tonic`, `h2`, and `reqwest`.
32+
//
33+
// Note: This filtering will also drop logs from these components even when
34+
// they are used outside of the OTLP Exporter.
35+
let filter_otel = EnvFilter::new("info")
36+
.add_directive("hyper=off".parse().unwrap())
37+
.add_directive("tonic=off".parse().unwrap())
38+
.add_directive("h2=off".parse().unwrap())
39+
.add_directive("reqwest=off".parse().unwrap());
40+
let otel_layer = layer::OpenTelemetryTracingBridge::new(&provider).with_filter(filter_otel);
41+
42+
// Create a new tracing::Fmt layer to print the logs to stdout. It has a
43+
// default filter of `info` level and above, and `debug` and above for logs
44+
// from OpenTelemetry crates. The filter levels can be customized as needed.
45+
let filter_fmt = EnvFilter::new("error").add_directive("opentelemetry=debug".parse().unwrap());
46+
let fmt_layer = tracing_subscriber::fmt::layer()
47+
.with_thread_names(true)
48+
.with_filter(filter_fmt);
49+
50+
tracing_subscriber::registry()
51+
.with(otel_layer)
52+
.with(fmt_layer)
53+
.init();
54+
55+
info!(name: "my-event-name", target: "my-system", event_id = 20, user_name = "otel", user_email = "[email protected]", message = "This is an example message");
56+
error!(name: "my-event-name", target: "my-system", event_id = 50, user_name = "otel", user_email = "[email protected]", message = "This is an example message");
57+
let _ = provider.shutdown();
58+
}
59+
60+
/// A log processor that enriches log records with additional attributes before
61+
/// delegating to an underlying processor.
62+
///
63+
/// If this were implemented as a standalone processor in a chain (e.g.,
64+
/// EnrichmentProcessor -> SimpleLogProcessor), the performance benefits of the
65+
/// `event_enabled` check would be nullified. Here's why:
66+
///
67+
/// - The `event_enabled` method is crucial for performance - it allows processors
68+
/// to skip expensive operations for logs that will ultimately be filtered out
69+
/// - A standalone EnrichmentProcessor would need to implement `event_enabled`,
70+
/// but it has no knowledge of downstream filtering logic
71+
/// - It would have to return `true` by default, causing unnecessary enrichment
72+
/// work even for logs that the downstream processor will discard
73+
///
74+
/// Because this processor wraps another, it must delegate all trait methods
75+
/// to the underlying processor. This ensures the underlying processor receives
76+
/// all necessary lifecycle events.
77+
#[derive(Debug)]
78+
pub struct EnrichmentLogProcessor<P: LogProcessor> {
79+
/// The wrapped processor that will receive enriched log records
80+
delegate: P,
81+
}
82+
83+
impl<P: LogProcessor> EnrichmentLogProcessor<P> {
84+
pub fn new(delegate: P) -> EnrichmentLogProcessor<P> {
85+
EnrichmentLogProcessor { delegate }
86+
}
87+
}
88+
89+
impl<P: LogProcessor> LogProcessor for EnrichmentLogProcessor<P> {
90+
fn emit(&self, data: &mut SdkLogRecord, instrumentation: &InstrumentationScope) {
91+
data.add_attribute("enriched", true);
92+
self.delegate.emit(data, instrumentation);
93+
}
94+
95+
fn force_flush(&self) -> OTelSdkResult {
96+
self.delegate.force_flush()
97+
}
98+
99+
fn shutdown_with_timeout(&self, timeout: std::time::Duration) -> OTelSdkResult {
100+
self.delegate.shutdown_with_timeout(timeout)
101+
}
102+
103+
fn shutdown(&self) -> OTelSdkResult {
104+
self.delegate.shutdown()
105+
}
106+
107+
fn set_resource(&mut self, resource: &Resource) {
108+
self.delegate.set_resource(resource);
109+
}
110+
111+
fn event_enabled(&self, level: Severity, target: &str, name: Option<&str>) -> bool {
112+
self.delegate.event_enabled(level, target, name)
113+
}
114+
}

0 commit comments

Comments
 (0)