Skip to content

Commit 892032d

Browse files
feat: Add OpenTelemetrySpanExt methods for direct span event creation
Adds `add_otel_span_event` and `add_otel_span_event_with_timestamp` methods to the `OpenTelemetrySpanExt` trait. These methods allow users to add OpenTelemetry span events directly to a `tracing::Span`, including events with dynamic attribute keys, bypassing the limitations of the `tracing::event!` macro for such use cases. The signatures align with the upstream OpenTelemetry Span trait, using `T: Into<Cow<'static, str>>` for the event name. Includes: - Trait method definitions and implementations. - Corresponding test cases. - Updates to README.md. - New example `examples/opentelemetry-span-event.rs`. - Changelog entries.
1 parent b6701c1 commit 892032d

File tree

5 files changed

+267
-2
lines changed

5 files changed

+267
-2
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Unreleased
22

3+
### Added
4+
5+
- Add `OpenTelemetrySpanExt::add_otel_span_event` function to allow adding OpenTelemetry
6+
events directly to a `tracing::Span`, enabling the use of dynamic attribute keys.
7+
- Add `OpenTelemetrySpanExt::add_otel_span_event_with_timestamp` function, similar to
8+
`add_otel_span_event`, but allowing a specific `SystemTime` to be provided for the event.
9+
- Add `examples/opentelemetry-span-event.rs` demonstrating the usage of the new span event methods.
10+
311
# 0.30.0 (March 23, 2025)
412

513
### Breaking Changes

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ The crate provides the following types:
4242

4343
* [`OpenTelemetryLayer`] adds OpenTelemetry context to all `tracing` [span]s.
4444
* [`OpenTelemetrySpanExt`] allows OpenTelemetry parent trace information to be
45-
injected and extracted from a `tracing` [span].
45+
injected and extracted from a `tracing` [span]. It also provides methods
46+
to directly set span attributes (`set_attribute`), span status (`set_status`),
47+
and add OpenTelemetry events with dynamic attributes using the current time
48+
(`add_otel_span_event`) or a specific timestamp (`add_otel_span_event_with_timestamp`).
4649

4750
[`OpenTelemetryLayer`]: https://docs.rs/tracing-opentelemetry/latest/tracing_opentelemetry/struct.OpenTelemetryLayer.html
4851
[`OpenTelemetrySpanExt`]: https://docs.rs/tracing-opentelemetry/latest/tracing_opentelemetry/trait.OpenTelemetrySpanExt.html

examples/opentelemetry-span-event.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use opentelemetry::trace::TracerProvider as _;
2+
use opentelemetry::KeyValue;
3+
use opentelemetry_sdk::trace::SdkTracerProvider;
4+
use opentelemetry_stdout as stdout;
5+
use std::time::SystemTime;
6+
use tracing::{error, span};
7+
use tracing_opentelemetry::OpenTelemetrySpanExt;
8+
use tracing_subscriber::layer::SubscriberExt;
9+
use tracing_subscriber::Registry;
10+
11+
fn main() {
12+
// Create a new OpenTelemetry trace pipeline that prints to stdout
13+
let provider = SdkTracerProvider::builder()
14+
.with_simple_exporter(stdout::SpanExporter::default())
15+
.build();
16+
let tracer = provider.tracer("otel_span_event_example");
17+
18+
// Create a tracing layer with the configured tracer
19+
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
20+
21+
// Use the tracing subscriber `Registry`, or any other subscriber
22+
// that impls `LookupSpan`
23+
let subscriber = Registry::default().with(telemetry);
24+
25+
// Trace executed code
26+
tracing::subscriber::with_default(subscriber, || {
27+
// Spans will be sent to the configured OpenTelemetry exporter
28+
let root = span!(tracing::Level::INFO, "app_start", work_units = 2);
29+
let _enter = root.enter();
30+
31+
error!("This event will be logged in the root span via tracing::event!");
32+
33+
// --- Add custom OpenTelemetry Span Events ---
34+
let current_span = tracing::Span::current();
35+
36+
// Generate a dynamic request ID using the current time
37+
let request_id_from_time = SystemTime::now()
38+
.duration_since(SystemTime::UNIX_EPOCH)
39+
.map(|d| d.as_nanos())
40+
.unwrap_or(0)
41+
.to_string();
42+
43+
// Let's assume that this is coming from a request from a user
44+
let dynamic_attributes = vec![
45+
KeyValue::new("user.id", "example-user-456"),
46+
KeyValue::new("request.id", request_id_from_time), // Use timestamp-based ID
47+
];
48+
49+
// Add event with current timestamp using the extension trait
50+
current_span.add_otel_span_event(
51+
"Processing dynamic request data",
52+
dynamic_attributes.clone(),
53+
);
54+
println!("Added span event: 'Processing dynamic request data'");
55+
56+
// Add event with a specific, past timestamp
57+
let past_timestamp = SystemTime::now() - std::time::Duration::from_secs(1);
58+
current_span.add_otel_span_event_with_timestamp(
59+
"Historical data point processed",
60+
past_timestamp,
61+
vec![KeyValue::new("historical.record.id", 99)],
62+
);
63+
println!("Added span event: 'Historical data point processed' with past timestamp");
64+
});
65+
}

src/span_ext.rs

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
use crate::layer::WithContext;
2-
use opentelemetry::{trace::SpanContext, trace::Status, Context, Key, KeyValue, Value};
2+
use opentelemetry::{
3+
time,
4+
trace::{SpanContext, Status},
5+
Context, Key, KeyValue, Value,
6+
};
7+
use std::{borrow::Cow, time::SystemTime};
38

49
/// Utility functions to allow tracing [`Span`]s to accept and return
510
/// [OpenTelemetry] [`Context`]s.
@@ -152,6 +157,63 @@ pub trait OpenTelemetrySpanExt {
152157
/// app_root.set_status(Status::Ok);
153158
/// ```
154159
fn set_status(&self, status: Status);
160+
161+
/// Adds an OpenTelemetry event directly to this span, bypassing `tracing::event!`.
162+
/// This allows for adding events with dynamic attribute keys, similar to `set_attribute` for span attributes.
163+
/// Events are added with the current timestamp.
164+
///
165+
/// # Examples
166+
///
167+
/// ```rust
168+
/// use opentelemetry::{KeyValue};
169+
/// use tracing_opentelemetry::OpenTelemetrySpanExt;
170+
/// use tracing::Span;
171+
///
172+
/// let app_root = tracing::span!(tracing::Level::INFO, "processing_request");
173+
///
174+
/// let dynamic_attrs = vec![
175+
/// KeyValue::new("job_id", "job-123"),
176+
/// KeyValue::new("user.id", "user-xyz"),
177+
/// ];
178+
///
179+
/// // Add event using the extension method
180+
/// app_root.add_otel_span_event("job_started".to_string(), dynamic_attrs);
181+
///
182+
/// // ... perform work ...
183+
///
184+
/// app_root.add_otel_span_event("job_completed", vec![KeyValue::new("status", "success")]);
185+
/// ```
186+
fn add_otel_span_event<T>(&self, name: T, attributes: Vec<KeyValue>)
187+
where
188+
T: Into<Cow<'static, str>>;
189+
190+
/// Adds an OpenTelemetry event with a specific timestamp directly to this span.
191+
/// Similar to `add_otel_span_event`, but allows overriding the event timestamp.
192+
///
193+
/// # Examples
194+
///
195+
/// ```rust
196+
/// use opentelemetry::{KeyValue};
197+
/// use tracing_opentelemetry::OpenTelemetrySpanExt;
198+
/// use tracing::Span;
199+
/// use std::time::{Duration, SystemTime};
200+
/// use std::borrow::Cow;
201+
///
202+
/// let app_root = tracing::span!(tracing::Level::INFO, "historical_event_processing");
203+
///
204+
/// let event_time = SystemTime::now() - Duration::from_secs(60);
205+
/// let event_attrs = vec![KeyValue::new("record_id", "rec-456")];
206+
/// let event_name: Cow<'static, str> = "event_from_past".into();
207+
///
208+
/// app_root.add_otel_span_event_with_timestamp(event_name, event_time, event_attrs);
209+
/// ```
210+
fn add_otel_span_event_with_timestamp<T>(
211+
&self,
212+
name: T,
213+
timestamp: SystemTime,
214+
attributes: Vec<KeyValue>,
215+
) where
216+
T: Into<Cow<'static, str>>;
155217
}
156218

157219
impl OpenTelemetrySpanExt for tracing::Span {
@@ -237,4 +299,33 @@ impl OpenTelemetrySpanExt for tracing::Span {
237299
}
238300
});
239301
}
302+
303+
fn add_otel_span_event<T>(&self, name: T, attributes: Vec<KeyValue>)
304+
where
305+
T: Into<Cow<'static, str>>,
306+
{
307+
self.add_otel_span_event_with_timestamp(name, time::now(), attributes);
308+
}
309+
310+
fn add_otel_span_event_with_timestamp<T>(
311+
&self,
312+
name: T,
313+
timestamp: SystemTime,
314+
attributes: Vec<KeyValue>,
315+
) where
316+
T: Into<Cow<'static, str>>,
317+
{
318+
self.with_subscriber(move |(id, subscriber)| {
319+
let mut event = Some(opentelemetry::trace::Event::new(
320+
name, timestamp, attributes, 0,
321+
));
322+
if let Some(get_context) = subscriber.downcast_ref::<WithContext>() {
323+
get_context.with_context(subscriber, id, move |data, _tracer| {
324+
if let Some(event) = event.take() {
325+
data.builder.events.get_or_insert_with(Vec::new).push(event);
326+
}
327+
});
328+
}
329+
});
330+
}
240331
}

tests/span_ext.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,101 @@ fn set_status_helper(status: Status) -> SpanData {
7171

7272
spans.iter().find(|s| s.name == "root").unwrap().clone()
7373
}
74+
75+
#[test]
76+
fn test_add_otel_span_event() {
77+
let (_tracer, provider, exporter, subscriber) = test_tracer();
78+
79+
let event_name = "my_event";
80+
let event_attrs = vec![
81+
opentelemetry::KeyValue::new("event_key_1", "event_value_1"),
82+
opentelemetry::KeyValue::new("event_key_2", 123),
83+
];
84+
85+
tracing::subscriber::with_default(subscriber, || {
86+
let root = tracing::debug_span!("root");
87+
let _enter = root.enter(); // Enter span to make it current for the event addition
88+
89+
// Add the event using the new extension method
90+
root.add_otel_span_event(event_name, event_attrs.clone());
91+
});
92+
93+
drop(provider); // flush all spans
94+
let spans = exporter.0.lock().unwrap();
95+
96+
assert_eq!(spans.len(), 1, "Should have exported exactly one span.");
97+
let root_span_data = spans.first().unwrap();
98+
99+
assert_eq!(
100+
root_span_data.events.len(),
101+
1,
102+
"Span should have one event."
103+
);
104+
let event_data = root_span_data.events.first().unwrap();
105+
106+
assert_eq!(event_data.name, event_name, "Event name mismatch.");
107+
assert_eq!(
108+
event_data.attributes, event_attrs,
109+
"Event attributes mismatch."
110+
);
111+
}
112+
113+
#[test]
114+
fn test_add_otel_span_event_with_timestamp() {
115+
use std::time::{Duration, SystemTime};
116+
117+
let (_tracer, provider, exporter, subscriber) = test_tracer();
118+
119+
let event_name = "my_specific_time_event";
120+
let event_attrs = vec![opentelemetry::KeyValue::new("event_key_a", "value_a")];
121+
// Define a specific timestamp (e.g., 10 seconds ago)
122+
let specific_timestamp = SystemTime::now() - Duration::from_secs(10);
123+
124+
tracing::subscriber::with_default(subscriber, || {
125+
let root = tracing::debug_span!("root_with_timestamped_event");
126+
let _enter = root.enter();
127+
128+
// Add the event using the new extension method with the specific timestamp
129+
root.add_otel_span_event_with_timestamp(
130+
event_name,
131+
specific_timestamp,
132+
event_attrs.clone(),
133+
);
134+
});
135+
136+
drop(provider); // flush all spans
137+
let spans = exporter.0.lock().unwrap();
138+
139+
assert_eq!(spans.len(), 1, "Should have exported exactly one span.");
140+
let root_span_data = spans.first().unwrap();
141+
142+
assert_eq!(
143+
root_span_data.events.len(),
144+
1,
145+
"Span should have one event."
146+
);
147+
let event_data = root_span_data.events.first().unwrap();
148+
149+
assert_eq!(event_data.name, event_name, "Event name mismatch.");
150+
assert_eq!(
151+
event_data.attributes, event_attrs,
152+
"Event attributes mismatch."
153+
);
154+
155+
// Assert the timestamp matches the one we provided
156+
// Allow for a small tolerance due to potential precision differences during conversion
157+
let timestamp_diff = event_data
158+
.timestamp
159+
.duration_since(specific_timestamp)
160+
.unwrap_or_else(|_| {
161+
specific_timestamp
162+
.duration_since(event_data.timestamp)
163+
.unwrap_or_default()
164+
});
165+
assert!(
166+
timestamp_diff < Duration::from_millis(1),
167+
"Timestamp mismatch. Expected: {:?}, Got: {:?}",
168+
specific_timestamp,
169+
event_data.timestamp
170+
);
171+
}

0 commit comments

Comments
 (0)