Skip to content

Commit 36d0e77

Browse files
committed
feat(sdk): add otel.sdk.processor.log.processed self-diagnostics metric to BatchLogProcessor
1 parent dfdc478 commit 36d0e77

5 files changed

Lines changed: 313 additions & 28 deletions

File tree

docs/design/observability.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# SDK Self-Diagnostics via Metrics
2+
3+
Status:
4+
[Development](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/document-status.md)
5+
6+
The OpenTelemetry Rust SDK can emit metrics about its own internal state,
7+
following the [semantic conventions for SDK metrics](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/otel/sdk-metrics.md).
8+
9+
## Implemented Metrics
10+
11+
### `otel.sdk.processor.log.processed`
12+
13+
- **Instrument**: `Counter<u64>`
14+
- **Unit**: `{log_record}`
15+
- **Description**: The number of log records for which processing has finished,
16+
either successful or failed.
17+
- **Component**: `BatchLogProcessor`
18+
19+
**Attributes:**
20+
21+
| Attribute | Value |
22+
|-----------|-------|
23+
| `otel.component.type` | `batching_log_processor` |
24+
| `otel.component.name` | `batching_log_processor/{id}` (auto-assigned) |
25+
| `error.type` | `queue_full` when dropped due to full queue; `already_shutdown` when emitted after shutdown. Absent on success. |
26+
27+
The counter is incremented on every `emit()` call: once for successful
28+
enqueue, once with `error.type=queue_full` when dropped due to a full queue,
29+
and once with `error.type=already_shutdown` when emitted after the processor
30+
has been shut down.
31+
32+
## Feature Gate
33+
34+
Self-diagnostics metrics require the `experimental_metrics_bound_instruments`
35+
feature on `opentelemetry_sdk`. This feature is not enabled by default.
36+
37+
Without bound instruments, every `Counter::add()` call would need to resolve
38+
attributes to the internal aggregation state — roughly 50 ns per call on the
39+
`emit()` hot path. With bound instruments, attributes are resolved once at
40+
construction and subsequent `add()` calls are a single atomic increment at
41+
~1.8 ns. Since `emit()` is called for every log record in the application,
42+
this overhead matters. Bound instruments are what make self-diagnostics
43+
practical without measurable performance impact.
44+
45+
## Provider Initialization Order
46+
47+
The `BatchLogProcessor` obtains a `Meter` from the global `MeterProvider`
48+
during construction. Rust's `global::meter()` returns a snapshot — it does
49+
**not** retroactively upgrade if the global provider changes later.
50+
51+
For self-diagnostics to produce real data, the global `MeterProvider` must be
52+
set **before** creating the `LoggerProvider` (and its `BatchLogProcessor`).
53+
The recommended setup order is:
54+
55+
```rust
56+
// 1. MeterProvider first. Optionally set up a throwaway thread-local fmt
57+
// subscriber so that any internal logs emitted during MeterProvider
58+
// construction still appear on stdout. This is not required — without it
59+
// those few startup-time debug messages are simply lost.
60+
let meter_provider = {
61+
let _guard = tracing::subscriber::set_default(
62+
tracing_subscriber::fmt().with_env_filter("info").finish(),
63+
);
64+
let mp = init_metrics();
65+
global::set_meter_provider(mp.clone());
66+
mp
67+
}; // _guard drops here, removing the throwaway subscriber
68+
69+
// 2. LoggerProvider second (BatchLogProcessor picks up the real meter)
70+
let logger_provider = init_logs();
71+
72+
// 3. Full tracing subscriber (fmt + OTel bridge)
73+
tracing_subscriber::registry()
74+
.with(OpenTelemetryTracingBridge::new(&logger_provider))
75+
.with(fmt::layer())
76+
.init();
77+
78+
// 4. TracerProvider last (init logs captured by OTel pipeline)
79+
let tracer_provider = init_traces();
80+
global::set_tracer_provider(tracer_provider.clone());
81+
```
82+
83+
See the OTLP examples (`basic-otlp`, `basic-otlp-http`) for the full pattern
84+
including a temporary thread-local `fmt` subscriber during MeterProvider setup.
85+
86+
If the `MeterProvider` is set after the `LoggerProvider`, the counter will be
87+
backed by a no-op meter and silently produce nothing. This is harmless but
88+
means no self-diagnostics data.
89+
90+
## TODO
91+
92+
- Emit `otel.sdk.processor.log.queue.size` (current queue depth).
93+
- Emit `otel.sdk.processor.log.queue.capacity` (configured max queue size).
94+
- Emit `otel.sdk.exporter.log.exported` from log exporters.
95+
- Add self-diagnostics to `BatchSpanProcessor`.
96+
- Add self-diagnostics to `SimpleLogProcessor`.
97+
- Record logs lost when `shutdown_with_timeout` times out (the background
98+
thread may still hold unfinished exports and queued records).
99+
- Long-term: `global::meter()` currently returns a snapshot that does not
100+
reflect later calls to `set_meter_provider()`. Investigate whether the
101+
global meter can be made to pick up provider changes after the fact.

opentelemetry-otlp/examples/basic-otlp-http/src/main.rs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,27 @@ fn init_metrics() -> SdkMeterProvider {
6767

6868
#[tokio::main]
6969
async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
70+
// Setup MeterProvider first so that SDK components (e.g., BatchLogProcessor)
71+
// that obtain a Meter from the global MeterProvider during their
72+
// initialization will get a functional meter for self-diagnostics.
73+
// A temporary thread-local fmt subscriber is used to capture any logs
74+
// emitted during MeterProvider initialization to stdout.
75+
//
76+
// Set the global meter provider using a clone of the meter_provider.
77+
// Setting global meter provider is required if other parts of the application
78+
// uses global::meter() or global::meter_with_version() to get a meter.
79+
// Cloning simply creates a new reference to the same meter provider. It is
80+
// important to hold on to the meter_provider here, so as to invoke
81+
// shutdown on it when application ends.
82+
let meter_provider = {
83+
let _guard = tracing::subscriber::set_default(
84+
tracing_subscriber::fmt().with_env_filter("info").finish(),
85+
);
86+
let meter_provider = init_metrics();
87+
global::set_meter_provider(meter_provider.clone());
88+
meter_provider
89+
};
90+
7091
let logger_provider = init_logs();
7192

7293
// Create a new OpenTelemetryTracingBridge using the above LoggerProvider.
@@ -119,15 +140,6 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
119140
// shutdown on it when application ends.
120141
global::set_tracer_provider(tracer_provider.clone());
121142

122-
let meter_provider = init_metrics();
123-
// Set the global meter provider using a clone of the meter_provider.
124-
// Setting global meter provider is required if other parts of the application
125-
// uses global::meter() or global::meter_with_version() to get a meter.
126-
// Cloning simply creates a new reference to the same meter provider. It is
127-
// important to hold on to the meter_provider here, so as to invoke
128-
// shutdown on it when application ends.
129-
global::set_meter_provider(meter_provider.clone());
130-
131143
let common_scope_attributes = vec![KeyValue::new("scope-key", "scope-value")];
132144
let scope = InstrumentationScope::builder("basic")
133145
.with_version("1.0")
@@ -166,20 +178,23 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
166178

167179
info!(target: "my-target", "hello from {}. My price is {}", "apple", 1.99);
168180

169-
// Collect all shutdown errors
181+
// Collect all shutdown errors.
182+
// Shutdown order: tracer first, then logger, then meter.
183+
// MeterProvider is shut down last because the LoggerProvider's
184+
// BatchLogProcessor may emit self-diagnostic metrics during its shutdown.
170185
let mut shutdown_errors = Vec::new();
171186
if let Err(e) = tracer_provider.shutdown() {
172187
shutdown_errors.push(format!("tracer provider: {e}"));
173188
}
174189

175-
if let Err(e) = meter_provider.shutdown() {
176-
shutdown_errors.push(format!("meter provider: {e}"));
177-
}
178-
179190
if let Err(e) = logger_provider.shutdown() {
180191
shutdown_errors.push(format!("logger provider: {e}"));
181192
}
182193

194+
if let Err(e) = meter_provider.shutdown() {
195+
shutdown_errors.push(format!("meter provider: {e}"));
196+
}
197+
183198
// Return an error if any shutdown failed
184199
if !shutdown_errors.is_empty() {
185200
return Err(format!(

opentelemetry-otlp/examples/basic-otlp/src/main.rs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,27 @@ fn init_logs() -> SdkLoggerProvider {
5858

5959
#[tokio::main]
6060
async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
61+
// Setup MeterProvider first so that SDK components (e.g., BatchLogProcessor)
62+
// that obtain a Meter from the global MeterProvider during their
63+
// initialization will get a functional meter for self-diagnostics.
64+
// A temporary thread-local fmt subscriber is used to capture any logs
65+
// emitted during MeterProvider initialization to stdout.
66+
//
67+
// Set the global meter provider using a clone of the meter_provider.
68+
// Setting global meter provider is required if other parts of the application
69+
// uses global::meter() or global::meter_with_version() to get a meter.
70+
// Cloning simply creates a new reference to the same meter provider. It is
71+
// important to hold on to the meter_provider here, so as to invoke
72+
// shutdown on it when application ends.
73+
let meter_provider = {
74+
let _guard = tracing::subscriber::set_default(
75+
tracing_subscriber::fmt().with_env_filter("info").finish(),
76+
);
77+
let meter_provider = init_metrics();
78+
global::set_meter_provider(meter_provider.clone());
79+
meter_provider
80+
};
81+
6182
let logger_provider = init_logs();
6283

6384
// Create a new OpenTelemetryTracingBridge using the above LoggerProvider.
@@ -110,15 +131,6 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
110131
// shutdown on it when application ends.
111132
global::set_tracer_provider(tracer_provider.clone());
112133

113-
let meter_provider = init_metrics();
114-
// Set the global meter provider using a clone of the meter_provider.
115-
// Setting global meter provider is required if other parts of the application
116-
// uses global::meter() or global::meter_with_version() to get a meter.
117-
// Cloning simply creates a new reference to the same meter provider. It is
118-
// important to hold on to the meter_provider here, so as to invoke
119-
// shutdown on it when application ends.
120-
global::set_meter_provider(meter_provider.clone());
121-
122134
let common_scope_attributes = vec![KeyValue::new("scope-key", "scope-value")];
123135
let scope = InstrumentationScope::builder("basic")
124136
.with_version("1.0")
@@ -156,20 +168,23 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
156168

157169
info!(name: "my-event", target: "my-target", "hello from {}. My price is {}", "apple", 1.99);
158170

159-
// Collect all shutdown errors
171+
// Collect all shutdown errors.
172+
// Shutdown order: tracer first, then logger, then meter.
173+
// MeterProvider is shut down last because the LoggerProvider's
174+
// BatchLogProcessor may emit self-diagnostic metrics during its shutdown.
160175
let mut shutdown_errors = Vec::new();
161176
if let Err(e) = tracer_provider.shutdown() {
162177
shutdown_errors.push(format!("tracer provider: {e}"));
163178
}
164179

165-
if let Err(e) = meter_provider.shutdown() {
166-
shutdown_errors.push(format!("meter provider: {e}"));
167-
}
168-
169180
if let Err(e) = logger_provider.shutdown() {
170181
shutdown_errors.push(format!("logger provider: {e}"));
171182
}
172183

184+
if let Err(e) = meter_provider.shutdown() {
185+
shutdown_errors.push(format!("meter provider: {e}"));
186+
}
187+
173188
// Return an error if any shutdown failed
174189
if !shutdown_errors.is_empty() {
175190
return Err(format!(

0 commit comments

Comments
 (0)