Skip to main content

sc_observability_types/
tracing.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{ActionName, ErrorCode, StateName, TargetCategory, ValueValidationError, error_codes};
6
7/// Validated 32-character lowercase hexadecimal trace identifier.
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct TraceId(String);
10
11impl TraceId {
12    /// Creates a validated lowercase hexadecimal trace identifier.
13    ///
14    /// # Errors
15    ///
16    /// Returns [`ValueValidationError`] when the trace identifier is not a
17    /// 32-character lowercase hexadecimal value.
18    pub fn new(value: impl Into<String>) -> Result<Self, ValueValidationError> {
19        let value = value.into();
20        validate_lower_hex(
21            &value,
22            crate::constants::TRACE_ID_LEN,
23            &error_codes::TRACE_ID_INVALID,
24        )?;
25        Ok(Self(value))
26    }
27
28    /// Returns the underlying lowercase hexadecimal trace identifier.
29    #[must_use]
30    pub fn as_str(&self) -> &str {
31        &self.0
32    }
33}
34
35impl fmt::Display for TraceId {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        f.write_str(&self.0)
38    }
39}
40
41impl AsRef<str> for TraceId {
42    fn as_ref(&self) -> &str {
43        self.as_str()
44    }
45}
46
47impl TryFrom<String> for TraceId {
48    type Error = ValueValidationError;
49
50    fn try_from(value: String) -> Result<Self, Self::Error> {
51        Self::new(value)
52    }
53}
54
55/// Validated 16-character lowercase hexadecimal span identifier.
56#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
57pub struct SpanId(String);
58
59impl SpanId {
60    /// Creates a validated lowercase hexadecimal span identifier.
61    ///
62    /// # Errors
63    ///
64    /// Returns [`ValueValidationError`] when the span identifier is not a
65    /// 16-character lowercase hexadecimal value.
66    pub fn new(value: impl Into<String>) -> Result<Self, ValueValidationError> {
67        let value = value.into();
68        validate_lower_hex(
69            &value,
70            crate::constants::SPAN_ID_LEN,
71            &error_codes::SPAN_ID_INVALID,
72        )?;
73        Ok(Self(value))
74    }
75
76    /// Returns the underlying lowercase hexadecimal span identifier.
77    #[must_use]
78    pub fn as_str(&self) -> &str {
79        &self.0
80    }
81}
82
83impl fmt::Display for SpanId {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        f.write_str(&self.0)
86    }
87}
88
89impl AsRef<str> for SpanId {
90    fn as_ref(&self) -> &str {
91        self.as_str()
92    }
93}
94
95impl TryFrom<String> for SpanId {
96    type Error = ValueValidationError;
97
98    fn try_from(value: String) -> Result<Self, Self::Error> {
99        Self::new(value)
100    }
101}
102
103pub(crate) fn validate_lower_hex(
104    value: &str,
105    expected_len: usize,
106    code: &ErrorCode,
107) -> Result<(), ValueValidationError> {
108    if value.len() != expected_len {
109        return Err(ValueValidationError::with_code(
110            code.clone(),
111            format!("value must be {expected_len} lowercase hex characters"),
112        ));
113    }
114    if value
115        .chars()
116        .all(|ch| ch.is_ascii_hexdigit() && !ch.is_ascii_uppercase())
117    {
118        Ok(())
119    } else {
120        Err(ValueValidationError::with_code(
121            code.clone(),
122            "value must contain lowercase hex characters only",
123        ))
124    }
125}
126
127/// Generic trace correlation context shared by logs, spans, and observations.
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129pub struct TraceContext {
130    /// W3C-compatible trace identifier.
131    pub trace_id: TraceId,
132    /// Current span identifier.
133    pub span_id: SpanId,
134    /// Optional parent span identifier.
135    pub parent_span_id: Option<SpanId>,
136}
137
138/// Typed description of an entity moving from one state to another.
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct StateTransition {
141    /// Stable category describing what changed, such as `task` or `subagent`.
142    pub entity_kind: TargetCategory,
143    /// Optional caller-owned identifier for the entity that changed.
144    pub entity_id: Option<String>,
145    /// Previous stable state label.
146    pub from_state: StateName,
147    /// New stable state label.
148    pub to_state: StateName,
149    /// Optional human-readable explanation for why the transition occurred.
150    pub reason: Option<String>,
151    /// Optional action or event name that triggered the transition.
152    pub trigger: Option<ActionName>,
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn trace_and_span_ids_validate_w3c_shapes() {
161        assert!(TraceId::new("0123456789abcdef0123456789abcdef").is_ok());
162        assert_eq!(
163            TraceId::try_from("0123456789abcdef0123456789abcdef".to_string())
164                .expect("valid trace id")
165                .as_ref(),
166            "0123456789abcdef0123456789abcdef"
167        );
168        assert_eq!(
169            TraceId::new("0123456789abcdef0123456789abcdef")
170                .expect("valid trace id")
171                .to_string(),
172            "0123456789abcdef0123456789abcdef"
173        );
174        let short_trace = TraceId::new("0123456789abcdef0123456789abcde")
175            .expect_err("short trace id should fail");
176        assert_eq!(short_trace.code(), &error_codes::TRACE_ID_INVALID);
177        let uppercase_trace = TraceId::new("0123456789ABCDEF0123456789abcdef")
178            .expect_err("uppercase trace id should fail");
179        assert_eq!(uppercase_trace.code(), &error_codes::TRACE_ID_INVALID);
180
181        assert!(SpanId::new("0123456789abcdef").is_ok());
182        assert_eq!(
183            SpanId::try_from("0123456789abcdef".to_string())
184                .expect("valid span id")
185                .as_ref(),
186            "0123456789abcdef"
187        );
188        assert_eq!(
189            SpanId::new("0123456789abcdef")
190                .expect("valid span id")
191                .to_string(),
192            "0123456789abcdef"
193        );
194        let short_span = SpanId::new("0123456789abcde").expect_err("short span id should fail");
195        assert_eq!(short_span.code(), &error_codes::SPAN_ID_INVALID);
196        let uppercase_span =
197            SpanId::new("0123456789ABCDEf").expect_err("uppercase span id should fail");
198        assert_eq!(uppercase_span.code(), &error_codes::SPAN_ID_INVALID);
199    }
200}