Skip to content

feat: Add OpenTelemetrySpanExt methods for direct span event creation #201

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Unreleased

### Added

- Add `OpenTelemetrySpanExt::add_event` and `OpenTelemetrySpanExt::add_event_with_timestamp`
functions to allow adding OpenTelemetry events directly to a `tracing::Span`, enabling the use of dynamic attribute keys
and custom event timestamps.

# 0.30.0 (March 23, 2025)

### Breaking Changes
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ The crate provides the following types:

* [`OpenTelemetryLayer`] adds OpenTelemetry context to all `tracing` [span]s.
* [`OpenTelemetrySpanExt`] allows OpenTelemetry parent trace information to be
injected and extracted from a `tracing` [span].
injected and extracted from a `tracing` [span]. It also provides methods
to directly set span attributes (`set_attribute`), span status (`set_status`),
and add OpenTelemetry events with dynamic attributes using the current time
(`add_event`) or a specific timestamp (`add_event_with_timestamp`).

[`OpenTelemetryLayer`]: https://docs.rs/tracing-opentelemetry/latest/tracing_opentelemetry/struct.OpenTelemetryLayer.html
[`OpenTelemetrySpanExt`]: https://docs.rs/tracing-opentelemetry/latest/tracing_opentelemetry/trait.OpenTelemetrySpanExt.html
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
//! special fields are:
//!
//! * `otel.name`: Override the span name sent to OpenTelemetry exporters.
//! Setting this field is useful if you want to display non-static information
//! in your span name.
//! Setting this field is useful if you want to display non-static information
//! in your span name.
//! * `otel.kind`: Set the span kind to one of the supported OpenTelemetry [span kinds].
//! * `otel.status_code`: Set the span status code to one of the supported OpenTelemetry [span status codes].
//! * `otel.status_message`: Set the span status message.
Expand Down
184 changes: 138 additions & 46 deletions src/span_ext.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
use crate::layer::WithContext;
use opentelemetry::{trace::SpanContext, trace::Status, Context, Key, KeyValue, Value};
use opentelemetry::{
time,
trace::{SpanContext, Status},
Context, Key, KeyValue, Value,
};
use std::{borrow::Cow, time::SystemTime};

/// Utility functions to allow tracing [`Span`]s to accept and return
/// [OpenTelemetry] [`Context`]s.
Expand Down Expand Up @@ -152,20 +157,76 @@ pub trait OpenTelemetrySpanExt {
/// app_root.set_status(Status::Ok);
/// ```
fn set_status(&self, status: Status);

/// Adds an OpenTelemetry event directly to this span, bypassing `tracing::event!`.
/// This allows for adding events with dynamic attribute keys, similar to `set_attribute` for span attributes.
/// Events are added with the current timestamp.
///
/// # Examples
///
/// ```rust
/// use opentelemetry::{KeyValue};
/// use tracing_opentelemetry::OpenTelemetrySpanExt;
/// use tracing::Span;
///
/// let app_root = tracing::span!(tracing::Level::INFO, "processing_request");
///
/// let dynamic_attrs = vec![
/// KeyValue::new("job_id", "job-123"),
/// KeyValue::new("user.id", "user-xyz"),
/// ];
///
/// // Add event using the extension method
/// app_root.add_event("job_started".to_string(), dynamic_attrs);
///
/// // ... perform work ...
///
/// app_root.add_event("job_completed", vec![KeyValue::new("status", "success")]);
/// ```
fn add_event(&self, name: impl Into<Cow<'static, str>>, attributes: Vec<KeyValue>);

/// Adds an OpenTelemetry event with a specific timestamp directly to this span.
/// Similar to `add_event`, but allows overriding the event timestamp.
///
/// # Examples
///
/// ```rust
/// use opentelemetry::{KeyValue};
/// use tracing_opentelemetry::OpenTelemetrySpanExt;
/// use tracing::Span;
/// use std::time::{Duration, SystemTime};
/// use std::borrow::Cow;
///
/// let app_root = tracing::span!(tracing::Level::INFO, "historical_event_processing");
///
/// let event_time = SystemTime::now() - Duration::from_secs(60);
/// let event_attrs = vec![KeyValue::new("record_id", "rec-456")];
/// let event_name: Cow<'static, str> = "event_from_past".into();
///
/// app_root.add_event_with_timestamp(event_name, event_time, event_attrs);
/// ```
fn add_event_with_timestamp(
&self,
name: impl Into<Cow<'static, str>>,
timestamp: SystemTime,
attributes: Vec<KeyValue>,
);
}

impl OpenTelemetrySpanExt for tracing::Span {
fn set_parent(&self, cx: Context) {
let mut cx = Some(cx);
self.with_subscriber(move |(id, subscriber)| {
if let Some(get_context) = subscriber.downcast_ref::<WithContext>() {
get_context.with_context(subscriber, id, move |data, _tracer| {
if let Some(cx) = cx.take() {
data.parent_cx = cx;
data.builder.sampling_result = None;
}
});
}
let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
return;
};
get_context.with_context(subscriber, id, move |data, _tracer| {
let Some(cx) = cx.take() else {
return;
};
data.parent_cx = cx;
data.builder.sampling_result = None;
});
});
}

Expand All @@ -178,63 +239,94 @@ impl OpenTelemetrySpanExt for tracing::Span {
let mut cx = Some(cx);
let mut att = Some(attributes);
self.with_subscriber(move |(id, subscriber)| {
if let Some(get_context) = subscriber.downcast_ref::<WithContext>() {
get_context.with_context(subscriber, id, move |data, _tracer| {
if let Some(cx) = cx.take() {
let attr = att.take().unwrap_or_default();
let follows_link = opentelemetry::trace::Link::new(cx, attr, 0);
data.builder
.links
.get_or_insert_with(|| Vec::with_capacity(1))
.push(follows_link);
}
});
}
let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
return;
};
get_context.with_context(subscriber, id, move |data, _tracer| {
let Some(cx) = cx.take() else {
return;
};
let attr = att.take().unwrap_or_default();
let follows_link = opentelemetry::trace::Link::new(cx, attr, 0);
data.builder
.links
.get_or_insert_with(|| Vec::with_capacity(1))
.push(follows_link);
});
});
}
}

fn context(&self) -> Context {
let mut cx = None;
self.with_subscriber(|(id, subscriber)| {
if let Some(get_context) = subscriber.downcast_ref::<WithContext>() {
get_context.with_context(subscriber, id, |builder, tracer| {
cx = Some(tracer.sampled_context(builder));
})
}
let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
return;
};
get_context.with_context(subscriber, id, |builder, tracer| {
cx = Some(tracer.sampled_context(builder));
})
});

cx.unwrap_or_default()
}

fn set_attribute(&self, key: impl Into<Key>, value: impl Into<Value>) {
self.with_subscriber(move |(id, subscriber)| {
if let Some(get_context) = subscriber.downcast_ref::<WithContext>() {
let mut key = Some(key.into());
let mut value = Some(value.into());
get_context.with_context(subscriber, id, move |builder, _| {
if builder.builder.attributes.is_none() {
builder.builder.attributes = Some(Default::default());
}
builder
.builder
.attributes
.as_mut()
.unwrap()
.push(KeyValue::new(key.take().unwrap(), value.take().unwrap()));
})
}
let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
return;
};
let mut key = Some(key.into());
let mut value = Some(value.into());
get_context.with_context(subscriber, id, move |builder, _| {
if builder.builder.attributes.is_none() {
builder.builder.attributes = Some(Default::default());
}
builder
.builder
.attributes
.as_mut()
.unwrap()
.push(KeyValue::new(key.take().unwrap(), value.take().unwrap()));
})
});
}

fn set_status(&self, status: Status) {
self.with_subscriber(move |(id, subscriber)| {
let mut status = Some(status);
if let Some(get_context) = subscriber.downcast_ref::<WithContext>() {
get_context.with_context(subscriber, id, move |builder, _| {
builder.builder.status = status.take().unwrap();
});
}
let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
return;
};
get_context.with_context(subscriber, id, move |builder, _| {
builder.builder.status = status.take().unwrap();
});
});
}

fn add_event(&self, name: impl Into<Cow<'static, str>>, attributes: Vec<KeyValue>) {
self.add_event_with_timestamp(name, time::now(), attributes);
}

fn add_event_with_timestamp(
&self,
name: impl Into<Cow<'static, str>>,
timestamp: SystemTime,
attributes: Vec<KeyValue>,
) {
self.with_subscriber(move |(id, subscriber)| {
let mut event = Some(opentelemetry::trace::Event::new(
name, timestamp, attributes, 0,
));
let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
return;
};
get_context.with_context(subscriber, id, move |data, _tracer| {
let Some(event) = event.take() else {
return;
};
data.builder.events.get_or_insert_with(Vec::new).push(event);
});
});
}
}
2 changes: 1 addition & 1 deletion tests/metrics_publishing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ where
.data_points
.iter()
.map(|data_point| data_point.value)
.last()
.next_back()
.unwrap()
);

Expand Down
94 changes: 94 additions & 0 deletions tests/span_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,97 @@ fn set_status_helper(status: Status) -> SpanData {

spans.iter().find(|s| s.name == "root").unwrap().clone()
}

#[test]
fn test_add_event() {
let (_tracer, provider, exporter, subscriber) = test_tracer();

let event_name = "my_event";
let event_attrs = vec![
opentelemetry::KeyValue::new("event_key_1", "event_value_1"),
opentelemetry::KeyValue::new("event_key_2", 123),
];

tracing::subscriber::with_default(subscriber, || {
let root = tracing::debug_span!("root");
let _enter = root.enter(); // Enter span to make it current for the event addition

// Add the event using the new extension method
root.add_event(event_name, event_attrs.clone());
});

drop(provider); // flush all spans
let spans = exporter.0.lock().unwrap();

assert_eq!(spans.len(), 1, "Should have exported exactly one span.");
let root_span_data = spans.first().unwrap();

assert_eq!(
root_span_data.events.len(),
1,
"Span should have one event."
);
let event_data = root_span_data.events.first().unwrap();

assert_eq!(event_data.name, event_name, "Event name mismatch.");
assert_eq!(
event_data.attributes, event_attrs,
"Event attributes mismatch."
);
}

#[test]
fn test_add_event_with_timestamp() {
use std::time::{Duration, SystemTime};

let (_tracer, provider, exporter, subscriber) = test_tracer();

let event_name = "my_specific_time_event";
let event_attrs = vec![opentelemetry::KeyValue::new("event_key_a", "value_a")];
// Define a specific timestamp (e.g., 10 seconds ago)
let specific_timestamp = SystemTime::now() - Duration::from_secs(10);

tracing::subscriber::with_default(subscriber, || {
let root = tracing::debug_span!("root_with_timestamped_event");
let _enter = root.enter();

// Add the event using the new extension method with the specific timestamp
root.add_event_with_timestamp(event_name, specific_timestamp, event_attrs.clone());
});

drop(provider); // flush all spans
let spans = exporter.0.lock().unwrap();

assert_eq!(spans.len(), 1, "Should have exported exactly one span.");
let root_span_data = spans.first().unwrap();

assert_eq!(
root_span_data.events.len(),
1,
"Span should have one event."
);
let event_data = root_span_data.events.first().unwrap();

assert_eq!(event_data.name, event_name, "Event name mismatch.");
assert_eq!(
event_data.attributes, event_attrs,
"Event attributes mismatch."
);

// Assert the timestamp matches the one we provided
// Allow for a small tolerance due to potential precision differences during conversion
let timestamp_diff = event_data
.timestamp
.duration_since(specific_timestamp)
.unwrap_or_else(|_| {
specific_timestamp
.duration_since(event_data.timestamp)
.unwrap_or_default()
});
assert!(
timestamp_diff < Duration::from_millis(1),
"Timestamp mismatch. Expected: {:?}, Got: {:?}",
specific_timestamp,
event_data.timestamp
);
}
Loading