1use std::fmt::Display;
7
8use opentelemetry::trace::{Status, TraceContextExt};
9use tracing::Span;
10use tracing_opentelemetry::OpenTelemetrySpanExt;
11
12pub trait SpanExt {
14 fn record_error<E: Display>(&self, error: &E);
16
17 fn record_exception(&self, error_type: &str, message: &str, stacktrace: Option<&str>);
19
20 fn record_result<T, E: Display>(&self, result: &Result<T, E>) -> &Self;
22
23 fn set_ok(&self);
25
26 fn set_error(&self, message: &str);
28
29 fn set_string_attribute(&self, key: &'static str, value: String);
31
32 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
90pub trait InstrumentedResult<T, E> {
102 fn record_to_span(self) -> Result<T, E>;
104
105 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
125pub struct TimingContext {
127 span: Span,
128 start: std::time::Instant,
129 _operation: String,
130}
131
132impl TimingContext {
133 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 pub fn elapsed(&self) -> std::time::Duration {
144 self.start.elapsed()
145 }
146
147 #[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 #[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}