Skip to main content

rskit_observability/
attribute.rs

1//! Span attribute helpers shared by tracing and OpenTelemetry integrations.
2
3use tracing::Span;
4use tracing_opentelemetry::OpenTelemetrySpanExt;
5
6/// Value that can be attached to an observability span attribute.
7#[derive(Debug, Clone, PartialEq)]
8#[non_exhaustive]
9pub enum SpanAttributeValue {
10    /// String attribute value.
11    String(String),
12    /// Signed integer attribute value.
13    I64(i64),
14    /// Floating-point attribute value.
15    F64(f64),
16    /// Boolean attribute value.
17    Bool(bool),
18}
19
20impl From<&str> for SpanAttributeValue {
21    fn from(value: &str) -> Self {
22        Self::String(value.to_owned())
23    }
24}
25
26impl From<String> for SpanAttributeValue {
27    fn from(value: String) -> Self {
28        Self::String(value)
29    }
30}
31
32impl From<&String> for SpanAttributeValue {
33    fn from(value: &String) -> Self {
34        Self::String(value.clone())
35    }
36}
37
38impl From<i64> for SpanAttributeValue {
39    fn from(value: i64) -> Self {
40        Self::I64(value)
41    }
42}
43
44impl From<i32> for SpanAttributeValue {
45    fn from(value: i32) -> Self {
46        Self::I64(i64::from(value))
47    }
48}
49
50impl From<u64> for SpanAttributeValue {
51    fn from(value: u64) -> Self {
52        match i64::try_from(value) {
53            Ok(value) => Self::I64(value),
54            Err(_) => Self::I64(i64::MAX),
55        }
56    }
57}
58
59impl From<usize> for SpanAttributeValue {
60    fn from(value: usize) -> Self {
61        match i64::try_from(value) {
62            Ok(value) => Self::I64(value),
63            Err(_) => Self::I64(i64::MAX),
64        }
65    }
66}
67
68impl From<f64> for SpanAttributeValue {
69    fn from(value: f64) -> Self {
70        Self::F64(value)
71    }
72}
73
74impl From<f32> for SpanAttributeValue {
75    fn from(value: f32) -> Self {
76        Self::F64(f64::from(value))
77    }
78}
79
80impl From<bool> for SpanAttributeValue {
81    fn from(value: bool) -> Self {
82        Self::Bool(value)
83    }
84}
85
86/// Set an OpenTelemetry span attribute on a tracing span.
87pub fn set_span_attribute(span: &Span, key: &'static str, value: impl Into<SpanAttributeValue>) {
88    set_opentelemetry_attribute(span, key, value.into());
89}
90
91/// Record a tracing span field and set the same OpenTelemetry span attribute.
92///
93/// `tracing` can only record fields declared when the span was created. If the
94/// field was not declared, the tracing record is ignored while the OpenTelemetry
95/// attribute is still set.
96pub fn record_span_attribute(span: &Span, key: &'static str, value: impl Into<SpanAttributeValue>) {
97    let value = value.into();
98    record_tracing_field(span, key, &value);
99    set_opentelemetry_attribute(span, key, value);
100}
101
102/// Set an OpenTelemetry attribute on the current span.
103pub fn set_current_span_attribute(key: &'static str, value: impl Into<SpanAttributeValue>) {
104    set_span_attribute(&Span::current(), key, value);
105}
106
107/// Record a tracing field and set an OpenTelemetry attribute on the current span.
108pub fn record_current_span_attribute(key: &'static str, value: impl Into<SpanAttributeValue>) {
109    record_span_attribute(&Span::current(), key, value);
110}
111
112fn record_tracing_field(span: &Span, key: &'static str, value: &SpanAttributeValue) {
113    match value {
114        SpanAttributeValue::String(value) => span.record(key, value.as_str()),
115        SpanAttributeValue::I64(value) => span.record(key, *value),
116        SpanAttributeValue::F64(value) => span.record(key, *value),
117        SpanAttributeValue::Bool(value) => span.record(key, *value),
118    };
119}
120
121fn set_opentelemetry_attribute(span: &Span, key: &'static str, value: SpanAttributeValue) {
122    match value {
123        SpanAttributeValue::String(value) => span.set_attribute(key, value),
124        SpanAttributeValue::I64(value) => span.set_attribute(key, value),
125        SpanAttributeValue::F64(value) => span.set_attribute(key, value),
126        SpanAttributeValue::Bool(value) => span.set_attribute(key, value),
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn unsigned_values_saturate_to_otlp_integer_range() {
136        assert_eq!(
137            SpanAttributeValue::from(u64::MAX),
138            SpanAttributeValue::I64(i64::MAX)
139        );
140        assert_eq!(SpanAttributeValue::from(7_u64), SpanAttributeValue::I64(7));
141        assert_eq!(
142            SpanAttributeValue::from(usize::MAX),
143            SpanAttributeValue::I64(i64::MAX)
144        );
145        assert_eq!(
146            SpanAttributeValue::from(7_usize),
147            SpanAttributeValue::I64(7)
148        );
149    }
150
151    #[test]
152    fn helpers_accept_common_attribute_values() {
153        let owned = String::from("owned");
154        assert_eq!(
155            SpanAttributeValue::from(owned.clone()),
156            SpanAttributeValue::String(owned.clone())
157        );
158        assert_eq!(
159            SpanAttributeValue::from(&owned),
160            SpanAttributeValue::String(owned)
161        );
162        assert_eq!(SpanAttributeValue::from(7_i32), SpanAttributeValue::I64(7));
163        assert_eq!(
164            SpanAttributeValue::from(1.25_f32),
165            SpanAttributeValue::F64(1.25)
166        );
167
168        let span = tracing::info_span!(
169            "attribute-test",
170            string = tracing::field::Empty,
171            integer = tracing::field::Empty,
172            float = tracing::field::Empty,
173            boolean = tracing::field::Empty,
174        );
175
176        record_span_attribute(&span, "string", "value");
177        record_span_attribute(&span, "integer", 42_i64);
178        record_span_attribute(&span, "float", 1.5_f64);
179        record_span_attribute(&span, "boolean", true);
180        set_span_attribute(&span, "otel.only", "value");
181
182        let _entered = span.enter();
183        set_current_span_attribute("current.otel", false);
184        record_current_span_attribute("boolean", false);
185    }
186}