platform_core/
telemetry_attrs.rs1use 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}