Skip to content

Commit 78e8247

Browse files
feat: Add OpenTelemetrySpanExt methods for direct span event creation
Adds `add_event` and `add_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 use `impl Into<Cow<'static, str>>` for the event name. Includes: - Trait method definitions and implementations. - Corresponding test cases. - Updates to README.md. - Changelog entries. Also incorporates refactoring based on review feedback: - Use let-else patterns to reduce rightward drift. - Rename methods for API simplification (`add_event`). - Use `impl trait` consistently in method parameters. - Remove example file (`examples/opentelemetry-span-event.rs`).
1 parent cec38b9 commit 78e8247

File tree

4 files changed

+242
-47
lines changed

4 files changed

+242
-47
lines changed

CHANGELOG.md

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

3+
### Added
4+
5+
- Add `OpenTelemetrySpanExt::add_event` and `OpenTelemetrySpanExt::add_event_with_timestamp`
6+
functions to allow adding OpenTelemetry events directly to a `tracing::Span`, enabling the use of dynamic attribute keys
7+
and custom event timestamps.
8+
39
# 0.30.0 (March 23, 2025)
410

511
### 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_event`) or a specific timestamp (`add_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

src/span_ext.rs

Lines changed: 138 additions & 46 deletions
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,20 +157,76 @@ 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_event("job_started".to_string(), dynamic_attrs);
181+
///
182+
/// // ... perform work ...
183+
///
184+
/// app_root.add_event("job_completed", vec![KeyValue::new("status", "success")]);
185+
/// ```
186+
fn add_event(&self, name: impl Into<Cow<'static, str>>, attributes: Vec<KeyValue>);
187+
188+
/// Adds an OpenTelemetry event with a specific timestamp directly to this span.
189+
/// Similar to `add_event`, but allows overriding the event timestamp.
190+
///
191+
/// # Examples
192+
///
193+
/// ```rust
194+
/// use opentelemetry::{KeyValue};
195+
/// use tracing_opentelemetry::OpenTelemetrySpanExt;
196+
/// use tracing::Span;
197+
/// use std::time::{Duration, SystemTime};
198+
/// use std::borrow::Cow;
199+
///
200+
/// let app_root = tracing::span!(tracing::Level::INFO, "historical_event_processing");
201+
///
202+
/// let event_time = SystemTime::now() - Duration::from_secs(60);
203+
/// let event_attrs = vec![KeyValue::new("record_id", "rec-456")];
204+
/// let event_name: Cow<'static, str> = "event_from_past".into();
205+
///
206+
/// app_root.add_event_with_timestamp(event_name, event_time, event_attrs);
207+
/// ```
208+
fn add_event_with_timestamp(
209+
&self,
210+
name: impl Into<Cow<'static, str>>,
211+
timestamp: SystemTime,
212+
attributes: Vec<KeyValue>,
213+
);
155214
}
156215

157216
impl OpenTelemetrySpanExt for tracing::Span {
158217
fn set_parent(&self, cx: Context) {
159218
let mut cx = Some(cx);
160219
self.with_subscriber(move |(id, subscriber)| {
161-
if let Some(get_context) = subscriber.downcast_ref::<WithContext>() {
162-
get_context.with_context(subscriber, id, move |data, _tracer| {
163-
if let Some(cx) = cx.take() {
164-
data.parent_cx = cx;
165-
data.builder.sampling_result = None;
166-
}
167-
});
168-
}
220+
let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
221+
return;
222+
};
223+
get_context.with_context(subscriber, id, move |data, _tracer| {
224+
let Some(cx) = cx.take() else {
225+
return;
226+
};
227+
data.parent_cx = cx;
228+
data.builder.sampling_result = None;
229+
});
169230
});
170231
}
171232

@@ -178,63 +239,94 @@ impl OpenTelemetrySpanExt for tracing::Span {
178239
let mut cx = Some(cx);
179240
let mut att = Some(attributes);
180241
self.with_subscriber(move |(id, subscriber)| {
181-
if let Some(get_context) = subscriber.downcast_ref::<WithContext>() {
182-
get_context.with_context(subscriber, id, move |data, _tracer| {
183-
if let Some(cx) = cx.take() {
184-
let attr = att.take().unwrap_or_default();
185-
let follows_link = opentelemetry::trace::Link::new(cx, attr, 0);
186-
data.builder
187-
.links
188-
.get_or_insert_with(|| Vec::with_capacity(1))
189-
.push(follows_link);
190-
}
191-
});
192-
}
242+
let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
243+
return;
244+
};
245+
get_context.with_context(subscriber, id, move |data, _tracer| {
246+
let Some(cx) = cx.take() else {
247+
return;
248+
};
249+
let attr = att.take().unwrap_or_default();
250+
let follows_link = opentelemetry::trace::Link::new(cx, attr, 0);
251+
data.builder
252+
.links
253+
.get_or_insert_with(|| Vec::with_capacity(1))
254+
.push(follows_link);
255+
});
193256
});
194257
}
195258
}
196259

197260
fn context(&self) -> Context {
198261
let mut cx = None;
199262
self.with_subscriber(|(id, subscriber)| {
200-
if let Some(get_context) = subscriber.downcast_ref::<WithContext>() {
201-
get_context.with_context(subscriber, id, |builder, tracer| {
202-
cx = Some(tracer.sampled_context(builder));
203-
})
204-
}
263+
let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
264+
return;
265+
};
266+
get_context.with_context(subscriber, id, |builder, tracer| {
267+
cx = Some(tracer.sampled_context(builder));
268+
})
205269
});
206270

207271
cx.unwrap_or_default()
208272
}
209273

210274
fn set_attribute(&self, key: impl Into<Key>, value: impl Into<Value>) {
211275
self.with_subscriber(move |(id, subscriber)| {
212-
if let Some(get_context) = subscriber.downcast_ref::<WithContext>() {
213-
let mut key = Some(key.into());
214-
let mut value = Some(value.into());
215-
get_context.with_context(subscriber, id, move |builder, _| {
216-
if builder.builder.attributes.is_none() {
217-
builder.builder.attributes = Some(Default::default());
218-
}
219-
builder
220-
.builder
221-
.attributes
222-
.as_mut()
223-
.unwrap()
224-
.push(KeyValue::new(key.take().unwrap(), value.take().unwrap()));
225-
})
226-
}
276+
let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
277+
return;
278+
};
279+
let mut key = Some(key.into());
280+
let mut value = Some(value.into());
281+
get_context.with_context(subscriber, id, move |builder, _| {
282+
if builder.builder.attributes.is_none() {
283+
builder.builder.attributes = Some(Default::default());
284+
}
285+
builder
286+
.builder
287+
.attributes
288+
.as_mut()
289+
.unwrap()
290+
.push(KeyValue::new(key.take().unwrap(), value.take().unwrap()));
291+
})
227292
});
228293
}
229294

230295
fn set_status(&self, status: Status) {
231296
self.with_subscriber(move |(id, subscriber)| {
232297
let mut status = Some(status);
233-
if let Some(get_context) = subscriber.downcast_ref::<WithContext>() {
234-
get_context.with_context(subscriber, id, move |builder, _| {
235-
builder.builder.status = status.take().unwrap();
236-
});
237-
}
298+
let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
299+
return;
300+
};
301+
get_context.with_context(subscriber, id, move |builder, _| {
302+
builder.builder.status = status.take().unwrap();
303+
});
304+
});
305+
}
306+
307+
fn add_event(&self, name: impl Into<Cow<'static, str>>, attributes: Vec<KeyValue>) {
308+
self.add_event_with_timestamp(name, time::now(), attributes);
309+
}
310+
311+
fn add_event_with_timestamp(
312+
&self,
313+
name: impl Into<Cow<'static, str>>,
314+
timestamp: SystemTime,
315+
attributes: Vec<KeyValue>,
316+
) {
317+
self.with_subscriber(move |(id, subscriber)| {
318+
let mut event = Some(opentelemetry::trace::Event::new(
319+
name, timestamp, attributes, 0,
320+
));
321+
let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
322+
return;
323+
};
324+
get_context.with_context(subscriber, id, move |data, _tracer| {
325+
let Some(event) = event.take() else {
326+
return;
327+
};
328+
data.builder.events.get_or_insert_with(Vec::new).push(event);
329+
});
238330
});
239331
}
240332
}

tests/span_ext.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,97 @@ 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_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_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_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_event_with_timestamp(event_name, specific_timestamp, event_attrs.clone());
130+
});
131+
132+
drop(provider); // flush all spans
133+
let spans = exporter.0.lock().unwrap();
134+
135+
assert_eq!(spans.len(), 1, "Should have exported exactly one span.");
136+
let root_span_data = spans.first().unwrap();
137+
138+
assert_eq!(
139+
root_span_data.events.len(),
140+
1,
141+
"Span should have one event."
142+
);
143+
let event_data = root_span_data.events.first().unwrap();
144+
145+
assert_eq!(event_data.name, event_name, "Event name mismatch.");
146+
assert_eq!(
147+
event_data.attributes, event_attrs,
148+
"Event attributes mismatch."
149+
);
150+
151+
// Assert the timestamp matches the one we provided
152+
// Allow for a small tolerance due to potential precision differences during conversion
153+
let timestamp_diff = event_data
154+
.timestamp
155+
.duration_since(specific_timestamp)
156+
.unwrap_or_else(|_| {
157+
specific_timestamp
158+
.duration_since(event_data.timestamp)
159+
.unwrap_or_default()
160+
});
161+
assert!(
162+
timestamp_diff < Duration::from_millis(1),
163+
"Timestamp mismatch. Expected: {:?}, Got: {:?}",
164+
specific_timestamp,
165+
event_data.timestamp
166+
);
167+
}

0 commit comments

Comments
 (0)