Skip to main content

platform_core/
telemetry_attrs.rs

1use crate::{CorrelationId, TraceContext};
2use serde_json::Value;
3use tracing::Span;
4use uuid::Uuid;
5
6pub const ATTR_CORRELATION_ID: &str = "lenso.correlation_id";
7pub const ATTR_STORY_ID: &str = "lenso.story_id";
8pub const ATTR_FUNCTION_RUN_ID: &str = "lenso.function_run_id";
9pub const ATTR_OUTBOX_EVENT_ID: &str = "lenso.outbox_event_id";
10pub const ATTR_EXECUTION_KIND: &str = "lenso.execution.kind";
11pub const ATTR_EXECUTION_NAME: &str = "lenso.execution.name";
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct RuntimeSpanAttributes {
15    pub correlation_id: String,
16    pub story_id: String,
17    pub execution_kind: String,
18    pub execution_name: String,
19    pub outbox_event_id: Option<String>,
20    pub function_run_id: Option<String>,
21}
22
23impl RuntimeSpanAttributes {
24    pub fn outbox(
25        correlation_id: impl Into<String>,
26        outbox_event_id: impl Into<String>,
27        execution_name: impl Into<String>,
28    ) -> Self {
29        let correlation_id = correlation_id.into();
30        Self {
31            story_id: correlation_id.clone(),
32            correlation_id,
33            execution_kind: "outbox_event".to_owned(),
34            execution_name: execution_name.into(),
35            outbox_event_id: Some(outbox_event_id.into()),
36            function_run_id: None,
37        }
38    }
39
40    pub fn function(
41        correlation_id: impl Into<String>,
42        function_run_id: impl Into<String>,
43        execution_name: impl Into<String>,
44    ) -> Self {
45        let correlation_id = correlation_id.into();
46        Self {
47            story_id: correlation_id.clone(),
48            correlation_id,
49            execution_kind: "function_run".to_owned(),
50            execution_name: execution_name.into(),
51            outbox_event_id: None,
52            function_run_id: Some(function_run_id.into()),
53        }
54    }
55}
56
57pub fn record_runtime_span_attributes(span: &Span, attrs: &RuntimeSpanAttributes) {
58    span.record(ATTR_CORRELATION_ID, attrs.correlation_id.as_str());
59    span.record(ATTR_STORY_ID, attrs.story_id.as_str());
60    span.record(ATTR_EXECUTION_KIND, attrs.execution_kind.as_str());
61    span.record(ATTR_EXECUTION_NAME, attrs.execution_name.as_str());
62
63    if let Some(outbox_event_id) = attrs.outbox_event_id.as_deref() {
64        span.record(ATTR_OUTBOX_EVENT_ID, outbox_event_id);
65    }
66    if let Some(function_run_id) = attrs.function_run_id.as_deref() {
67        span.record(ATTR_FUNCTION_RUN_ID, function_run_id);
68    }
69}
70
71pub fn trace_context_from_traceparent(value: &str) -> Option<TraceContext> {
72    let mut parts = value.split('-');
73    let version = parts.next()?;
74    let trace_id = parts.next()?;
75    let span_id = parts.next()?;
76    let flags = parts.next()?;
77
78    if parts.next().is_some()
79        || version.len() != 2
80        || trace_id.len() != 32
81        || span_id.len() != 16
82        || flags.len() != 2
83        || trace_id.chars().all(|char| char == '0')
84        || span_id.chars().all(|char| char == '0')
85        || ![version, trace_id, span_id, flags]
86            .iter()
87            .all(|part| part.chars().all(|char| char.is_ascii_hexdigit()))
88    {
89        return None;
90    }
91
92    Some(TraceContext {
93        trace_id: Some(trace_id.to_ascii_lowercase()),
94        span_id: Some(span_id.to_ascii_lowercase()),
95        baggage: Vec::new(),
96    })
97}
98
99pub fn generate_trace_context() -> TraceContext {
100    let trace_id = Uuid::now_v7().simple().to_string();
101    TraceContext {
102        span_id: Some(trace_id[..16].to_owned()),
103        trace_id: Some(trace_id),
104        baggage: Vec::new(),
105    }
106}
107
108pub fn trace_context_from_headers(headers: &Value) -> TraceContext {
109    headers
110        .get("trace")
111        .and_then(|trace| serde_json::from_value(trace.clone()).ok())
112        .unwrap_or_default()
113}
114
115pub fn trace_headers(trace: &TraceContext, correlation_id: &CorrelationId) -> Value {
116    serde_json::json!({
117        "correlation_id": correlation_id.0,
118        "trace": trace,
119    })
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn parses_valid_traceparent_into_existing_trace_context_shape() {
128        let trace = trace_context_from_traceparent(
129            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
130        )
131        .expect("traceparent should parse");
132
133        assert_eq!(
134            trace.trace_id.as_deref(),
135            Some("4bf92f3577b34da6a3ce929d0e0e4736")
136        );
137        assert_eq!(trace.span_id.as_deref(), Some("00f067aa0ba902b7"));
138    }
139
140    #[test]
141    fn rejects_invalid_traceparent_values() {
142        assert!(trace_context_from_traceparent("not-a-traceparent").is_none());
143        assert!(
144            trace_context_from_traceparent(
145                "00-00000000000000000000000000000000-00f067aa0ba902b7-01"
146            )
147            .is_none()
148        );
149        assert!(
150            trace_context_from_traceparent(
151                "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01"
152            )
153            .is_none()
154        );
155    }
156
157    #[test]
158    fn generates_trace_context_when_no_incoming_traceparent_exists() {
159        let trace = generate_trace_context();
160
161        assert_eq!(trace.trace_id.as_deref().unwrap_or_default().len(), 32);
162        assert_eq!(trace.span_id.as_deref().unwrap_or_default().len(), 16);
163    }
164
165    #[test]
166    fn builds_business_runtime_attributes_for_outbox_events() {
167        let attrs = RuntimeSpanAttributes::outbox("corr_1", "evt_1", "identity.user_registered.v1");
168
169        assert_eq!(attrs.correlation_id, "corr_1");
170        assert_eq!(attrs.story_id, "corr_1");
171        assert_eq!(attrs.execution_kind, "outbox_event");
172        assert_eq!(attrs.outbox_event_id.as_deref(), Some("evt_1"));
173        assert_eq!(attrs.function_run_id, None);
174    }
175
176    #[test]
177    fn builds_business_runtime_attributes_for_function_runs() {
178        let attrs = RuntimeSpanAttributes::function(
179            "corr_1",
180            "fnrun_1",
181            "notifications.send_welcome_email.v1",
182        );
183
184        assert_eq!(attrs.correlation_id, "corr_1");
185        assert_eq!(attrs.story_id, "corr_1");
186        assert_eq!(attrs.execution_kind, "function_run");
187        assert_eq!(attrs.function_run_id.as_deref(), Some("fnrun_1"));
188        assert_eq!(attrs.outbox_event_id, None);
189    }
190}