Skip to main content

otel_rs/
span.rs

1//! Span extensions for error recording and status management.
2//!
3//! Provides ergonomic helpers for recording errors, exceptions, and
4//! managing span status following OpenTelemetry semantic conventions.
5
6use std::fmt::Display;
7
8use opentelemetry::trace::{Status, TraceContextExt};
9use tracing::Span;
10use tracing_opentelemetry::OpenTelemetrySpanExt;
11
12/// Extension trait for enhanced span operations.
13pub trait SpanExt {
14    /// Record an error on the span and set status to Error.
15    fn record_error<E: Display>(&self, error: &E);
16
17    /// Record a structured exception following OTel semantic conventions.
18    fn record_exception(&self, error_type: &str, message: &str, stacktrace: Option<&str>);
19
20    /// Record a Result — if Err, records the error to the span.
21    fn record_result<T, E: Display>(&self, result: &Result<T, E>) -> &Self;
22
23    /// Set span status to OK.
24    fn set_ok(&self);
25
26    /// Set span status to Error with a message.
27    fn set_error(&self, message: &str);
28
29    /// Set a string attribute on the span.
30    fn set_string_attribute(&self, key: &'static str, value: String);
31
32    /// Set an i64 attribute on the span.
33    fn set_i64_attribute(&self, key: &'static str, value: i64);
34}
35
36impl SpanExt for Span {
37    fn record_error<E: Display>(&self, error: &E) {
38        let msg = error.to_string();
39        let ty = std::any::type_name::<E>();
40
41        self.set_error(&msg);
42        self.record("exception.message", msg.as_str());
43        self.record("exception.type", ty);
44        self.record("otel.status_code", "ERROR");
45
46        tracing::error!(
47            parent: self,
48            error.message = %msg,
49            error.type_name = %ty,
50            "Exception recorded on span"
51        );
52    }
53
54    fn record_exception(&self, error_type: &str, message: &str, stacktrace: Option<&str>) {
55        self.set_error(message);
56        self.record("exception.type", error_type);
57        self.record("exception.message", message);
58        if let Some(st) = stacktrace {
59            self.record("exception.stacktrace", st);
60        }
61        self.record("otel.status_code", "ERROR");
62    }
63
64    fn record_result<T, E: Display>(&self, result: &Result<T, E>) -> &Self {
65        if let Err(e) = result {
66            self.record_error(e);
67        }
68        self
69    }
70
71    fn set_ok(&self) {
72        self.record("otel.status_code", "OK");
73    }
74
75    fn set_error(&self, message: &str) {
76        let context = self.context();
77        let otel_span = context.span();
78        otel_span.set_status(Status::error(message.to_string()));
79    }
80
81    fn set_string_attribute(&self, key: &'static str, value: String) {
82        self.record(key, value.as_str());
83    }
84
85    fn set_i64_attribute(&self, key: &'static str, value: i64) {
86        self.record(key, value);
87    }
88}
89
90/// Extension trait for recording Result errors to the current span.
91///
92/// # Example
93///
94/// ```rust,ignore
95/// use otel_rs::InstrumentedResult;
96///
97/// async fn my_fn() -> Result<String, MyError> {
98///     fallible_call().await.record_to_span()
99/// }
100/// ```
101pub trait InstrumentedResult<T, E> {
102    /// Record any error to the current span, returning the result unchanged.
103    fn record_to_span(self) -> Result<T, E>;
104
105    /// Record any error to a specific span, returning the result unchanged.
106    fn record_to(self, span: &Span) -> Result<T, E>;
107}
108
109impl<T, E: Display> InstrumentedResult<T, E> for Result<T, E> {
110    fn record_to_span(self) -> Self {
111        if let Err(ref e) = self {
112            Span::current().record_error(e);
113        }
114        self
115    }
116
117    fn record_to(self, span: &Span) -> Self {
118        if let Err(ref e) = self {
119            span.record_error(e);
120        }
121        self
122    }
123}
124
125/// Context for tracking operation timing and recording to a span.
126pub struct TimingContext {
127    span: Span,
128    start: std::time::Instant,
129    _operation: String,
130}
131
132impl TimingContext {
133    /// Create a new timing context for an operation.
134    pub fn new(span: Span, operation: impl Into<String>) -> Self {
135        Self {
136            span,
137            start: std::time::Instant::now(),
138            _operation: operation.into(),
139        }
140    }
141
142    /// Get the elapsed duration.
143    pub fn elapsed(&self) -> std::time::Duration {
144        self.start.elapsed()
145    }
146
147    /// Finish timing and record duration to the span.
148    #[allow(clippy::cast_possible_truncation)]
149    pub fn finish(self) {
150        let dur = self.start.elapsed();
151        self.span.record("duration_ms", dur.as_millis() as i64);
152    }
153
154    /// Finish with a result, recording success/failure and timing.
155    #[allow(clippy::cast_possible_truncation)]
156    pub fn finish_with_result<T, E: Display>(self, result: &Result<T, E>) {
157        let dur = self.start.elapsed();
158        self.span.record("duration_ms", dur.as_millis() as i64);
159        self.span.record_result(result);
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn instrumented_result_ok() {
169        let result: Result<i32, &str> = Ok(42);
170        assert!(result.record_to_span().is_ok());
171    }
172
173    #[test]
174    fn instrumented_result_err() {
175        let result: Result<i32, &str> = Err("test error");
176        assert!(result.record_to_span().is_err());
177    }
178}