dmsc/observability/
propagation.rs

1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of DMSC.
4//! The DMSC project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//!     http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18#![allow(non_snake_case)]
19
20//! # Distributed Tracing Context Propagation
21//! 
22//! This module provides implementations for distributed tracing context propagation,
23//! following the W3C Trace Context specification. It allows for propagating trace
24//! information across service boundaries using HTTP headers.
25//! 
26//! ## Key Components
27//! 
28//! - **DMSCTraceContext**: Represents W3C Trace Context with trace ID, parent ID, and flags
29//! - **DMSCBaggage**: Represents baggage for carrying additional cross-cutting concerns
30//! - **DMSCContextCarrier**: Carries both trace context and baggage for propagation
31//! 
32//! ## Design Principles
33//! 
34//! 1. **W3C Compliance**: Implements the W3C Trace Context specification
35//! 2. **Baggage Support**: Provides baggage propagation for additional context
36//! 3. **HTTP Header Integration**: Supports extraction and injection from/to HTTP headers
37//! 4. **Serialization Support**: Implements Serialize and Deserialize for easy persistence
38//! 5. **Thread Safety**: All structs are cloneable for safe sharing across threads
39//! 6. **Sampling Support**: Includes trace sampling flag support
40//! 7. **Trace State**: Optional support for vendor-specific trace state
41//! 8. **Easy to Use**: Simple API for creating and manipulating trace contexts
42//! 
43//! ## Usage
44//! 
45//! ```rust
46//! use dmsc::prelude::*;
47//! use std::collections::HashMap;
48//! 
49//! fn example() {
50//!     // Create trace and span IDs
51//!     let trace_id = DMSCTraceId::generate();
52//!     let span_id = DMSCSpanId::generate();
53//!     
54//!     // Create a trace context
55//!     let mut trace_context = DMSCTraceContext::new(trace_id, span_id);
56//!     trace_context.set_sampled(true);
57//!     
58//!     // Create baggage
59//!     let mut baggage = DMSCBaggage::new();
60//!     baggage.insert("user_id".to_string(), "12345".to_string());
61//!     baggage.insert("request_id".to_string(), "abc123".to_string());
62//!     
63//!     // Create a context carrier
64//!     let carrier = DMSCContextCarrier::new()
65//!         .with_trace_context(trace_context)
66//!         .with_baggage(baggage);
67//!     
68//!     // Inject into HTTP headers
69//!     let mut headers = HashMap::new();
70//!     carrier.inject_into_headers(&mut headers);
71//!     println!("Headers: {:?}", headers);
72//!     
73//!     // Extract from HTTP headers
74//!     let extracted_carrier = DMSCContextCarrier::from_headers(&headers);
75//!     println!("Extracted trace context: {:?}", extracted_carrier.trace_context);
76//! }
77//! ```
78
79use std::collections::HashMap;
80use serde::{Serialize, Deserialize};
81use crate::observability::tracing::{DMSCTraceId, DMSCSpanId};
82
83/// W3C Trace Context propagation format
84///
85/// This struct represents a W3C Trace Context, which is used to propagate trace information
86/// across service boundaries. It follows the W3C Trace Context specification: https://www.w3.org/TR/trace-context/
87#[derive(Debug, Clone, Serialize, Deserialize)]
88#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
89pub struct DMSCTraceContext {
90    /// Trace context version
91    pub version: u8,
92    /// Trace ID for the entire trace
93    pub trace_id: DMSCTraceId,
94    /// Parent span ID
95    pub parent_id: DMSCSpanId,
96    /// Trace flags (bitmask)
97    pub trace_flags: u8,
98    /// Optional trace state for vendor-specific information
99    pub trace_state: Option<String>,
100}
101
102impl Default for DMSCTraceContext {
103    fn default() -> Self {
104        Self {
105            version: 0x00,
106            trace_id: DMSCTraceId::default(),
107            parent_id: DMSCSpanId::default(),
108            trace_flags: 0x01,
109            trace_state: None,
110        }
111    }
112}
113
114impl DMSCTraceContext {
115    /// Creates a new trace context with the given trace ID and parent span ID.
116    ///
117    /// # Parameters
118    ///
119    /// - `trace_id`: The trace ID for the trace
120    /// - `parent_id`: The parent span ID
121    ///
122    /// # Returns
123    ///
124    /// A new DMSCTraceContext instance
125    #[allow(dead_code)]
126    pub fn new(trace_id: DMSCTraceId, parent_id: DMSCSpanId) -> Self {
127        Self {
128            version: 0x00,
129            trace_id,
130            parent_id,
131            trace_flags: 0x01, // Sampled flag
132            trace_state: None,
133        }
134    }
135    
136    /// Parses a trace context from a W3C Trace Context header string.
137    ///
138    /// # Parameters
139    ///
140    /// - `header`: The traceparent header string in format "00-{trace-id}-{parent-id}-{trace-flags}"
141    ///
142    /// # Returns
143    ///
144    /// An Option containing the parsed DMSCTraceContext, or None if parsing failed
145    #[allow(dead_code)]
146    pub fn from_header(header: &str) -> Option<Self> {
147        let parts: Vec<&str> = header.split('-').collect();
148        if parts.len() != 4 {
149            return None;
150        }
151        
152        let version = u8::from_str_radix(parts[0], 16).ok()?;
153        let trace_id = DMSCTraceId::from_string(parts[1].to_string());
154        let parent_id = DMSCSpanId::from_string(parts[2].to_string());
155        let trace_flags = u8::from_str_radix(parts[3], 16).ok()?;
156        
157        Some(Self {
158            version,
159            trace_id,
160            parent_id,
161            trace_flags,
162            trace_state: None,
163        })
164    }
165    
166    /// Converts the trace context to a W3C Trace Context header string.
167    ///
168    /// # Returns
169    ///
170    /// A string in the format "00-{trace-id}-{parent-id}-{trace-flags}"
171    #[allow(dead_code)]
172    pub fn to_header(&self) -> String {
173        format!(
174            "{:02x}-{}-{}-{:02x}",
175            self.version,
176            self.trace_id.as_str(),
177            self.parent_id.as_str(),
178            self.trace_flags
179        )
180    }
181    
182    /// Checks if the trace is sampled.
183    ///
184    /// # Returns
185    ///
186    /// True if the sampled flag is set, false otherwise
187    #[allow(dead_code)]
188    pub fn is_sampled(&self) -> bool {
189        (self.trace_flags & 0x01) != 0
190    }
191    
192    /// Sets the sampled flag on the trace context.
193    ///
194    /// # Parameters
195    ///
196    /// - `sampled`: Whether the trace should be sampled
197    #[allow(dead_code)]
198    pub fn set_sampled(&mut self, sampled: bool) {
199        if sampled {
200            self.trace_flags |= 0x01;
201        } else {
202            self.trace_flags &= !0x01;
203        }
204    }
205}
206
207#[cfg(feature = "pyo3")]
208#[pyo3::prelude::pymethods]
209impl DMSCTraceContext {
210    #[new]
211    fn py_new() -> Self {
212        Self::default()
213    }
214
215    #[staticmethod]
216    #[pyo3(name = "from_header")]
217    fn py_from_header(header: String) -> Option<Self> {
218        Self::from_header(&header)
219    }
220
221    #[pyo3(name = "to_header")]
222    fn py_to_header(&self) -> String {
223        self.to_header()
224    }
225
226    #[pyo3(name = "is_sampled")]
227    fn py_is_sampled(&self) -> bool {
228        self.is_sampled()
229    }
230
231    fn sampled(&mut self, sampled: bool) {
232        self.set_sampled(sampled);
233    }
234}
235
236/// Baggage propagation for cross-cutting concerns.
237///
238/// This struct represents baggage, which is used to carry additional context information
239/// across service boundaries. It follows the W3C Baggage specification.
240#[derive(Debug, Clone, Serialize, Deserialize)]
241#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
242pub struct DMSCBaggage {
243    /// Map of baggage items
244    items: HashMap<String, String>,
245}
246
247impl Default for DMSCBaggage {
248    fn default() -> Self {
249        Self::new()
250    }
251}
252
253impl DMSCBaggage {
254    /// Creates a new empty baggage instance.
255    ///
256    /// # Returns
257    ///
258    /// A new DMSCBaggage instance
259    #[allow(dead_code)]
260    pub fn new() -> Self {
261        Self {
262            items: HashMap::new(),
263        }
264    }
265    
266    /// Inserts a key-value pair into the baggage.
267    ///
268    /// # Parameters
269    ///
270    /// - `key`: The baggage key
271    /// - `value`: The baggage value
272    #[allow(dead_code)]
273    pub fn insert(&mut self, key: String, value: String) {
274        self.items.insert(key, value);
275    }
276    
277    /// Gets a value from the baggage by key.
278    ///
279    /// # Parameters
280    ///
281    /// - `key`: The baggage key to look up
282    ///
283    /// # Returns
284    ///
285    /// An Option containing the value if found, or None otherwise
286    #[allow(dead_code)]
287    pub fn get(&self, key: &str) -> Option<&String> {
288        self.items.get(key)
289    }
290    
291    /// Removes a key-value pair from the baggage.
292    ///
293    /// # Parameters
294    ///
295    /// - `key`: The baggage key to remove
296    #[allow(dead_code)]
297    pub fn remove(&mut self, key: &str) {
298        self.items.remove(key);
299    }
300    
301    /// Parses baggage from a W3C Baggage header string.
302    ///
303    /// # Parameters
304    ///
305    /// - `header`: The baggage header string in format "key1=value1,key2=value2"
306    ///
307    /// # Returns
308    ///
309    /// A new DMSCBaggage instance with the parsed items
310    #[allow(dead_code)]
311    pub fn from_header(header: &str) -> Self {
312        let mut baggage = Self::new();
313        
314        for item in header.split(',') {
315            let item = item.trim();
316            if let Some(eq_pos) = item.find('=') {
317                let key = item[..eq_pos].trim().to_string();
318                let value = item[eq_pos + 1..].trim().to_string();
319                baggage.insert(key, value);
320            }
321        }
322        
323        baggage
324    }
325    
326    /// Converts the baggage to a W3C Baggage header string.
327    ///
328    /// # Returns
329    ///
330    /// A string in the format "key1=value1,key2=value2"
331    #[allow(dead_code)]
332    pub fn to_header(&self) -> String {
333        self.items
334            .iter()
335            .map(|(k, v)| format!("{k}={v}"))
336            .collect::<Vec<_>>()
337            .join(",")
338    }
339}
340
341#[cfg(feature = "pyo3")]
342#[pyo3::prelude::pymethods]
343impl DMSCBaggage {
344    #[new]
345    fn py_new() -> Self {
346        Self::new()
347    }
348
349    #[staticmethod]
350    #[pyo3(name = "from_header")]
351    fn py_from_header(header: String) -> Self {
352        Self::from_header(&header)
353    }
354
355    #[pyo3(name = "to_header")]
356    fn py_to_header(&self) -> String {
357        self.to_header()
358    }
359
360    fn add(&mut self, key: String, value: String) {
361        self.items.insert(key, value);
362    }
363
364    fn fetch(&self, key: String) -> Option<String> {
365        self.items.get(&key).cloned()
366    }
367
368    fn delete(&mut self, key: String) {
369        self.items.remove(&key);
370    }
371}
372
373/// Context carrier for distributed tracing.
374///
375/// This struct carries both trace context and baggage, providing a convenient way to
376/// extract and inject distributed tracing information from/to HTTP headers.
377#[allow(dead_code)]
378#[derive(Debug, Clone)]
379#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
380pub struct DMSCContextCarrier {
381    /// Trace context for the request
382    pub trace_context: Option<DMSCTraceContext>,
383    /// Baggage for additional context
384    pub baggage: DMSCBaggage,
385}
386
387impl Default for DMSCContextCarrier {
388    fn default() -> Self {
389        Self::new()
390    }
391}
392
393impl DMSCContextCarrier {
394    /// Creates a new empty context carrier.
395    ///
396    /// # Returns
397    ///
398    /// A new DMSCContextCarrier instance
399    #[allow(dead_code)]
400    pub fn new() -> Self {
401        Self {
402            trace_context: None,
403            baggage: DMSCBaggage::new(),
404        }
405    }
406    
407    /// Adds trace context to the carrier.
408    ///
409    /// # Parameters
410    ///
411    /// - `trace_context`: The trace context to add
412    ///
413    /// # Returns
414    ///
415    /// The updated DMSCContextCarrier instance
416    #[allow(dead_code)]
417    pub fn with_trace_context(mut self, trace_context: DMSCTraceContext) -> Self {
418        self.trace_context = Some(trace_context);
419        self
420    }
421    
422    /// Adds baggage to the carrier.
423    ///
424    /// # Parameters
425    ///
426    /// - `baggage`: The baggage to add
427    ///
428    /// # Returns
429    ///
430    /// The updated DMSCContextCarrier instance
431    #[allow(dead_code)]
432    pub fn with_baggage(mut self, baggage: DMSCBaggage) -> Self {
433        self.baggage = baggage;
434        self
435    }
436    
437    /// Creates a context carrier from tracing context.
438    ///
439    /// This method converts a thread-local DMSCTracingContext into a DMSCContextCarrier
440    /// that can be propagated across service boundaries.
441    ///
442    /// # Parameters
443    ///
444    /// - `tracing_context`: The tracing context to convert
445    ///
446    /// # Returns
447    ///
448    /// A new DMSCContextCarrier instance with trace context and baggage from the tracing context
449    #[allow(dead_code)]
450    pub fn from_tracing_context(tracing_context: &crate::observability::tracing::DMSCTracingContext) -> Self {
451        let mut carrier = Self::new();
452        
453        // Create trace context if trace ID and span ID are available
454        if let (Some(trace_id), Some(span_id)) = (
455            tracing_context.trace_id(),
456            tracing_context.span_id()
457        ) {
458            let trace_context = DMSCTraceContext::new(
459                trace_id.clone(),
460                span_id.clone()
461            );
462            carrier.trace_context = Some(trace_context);
463        }
464        
465        // Convert baggage from tracing context
466        let baggage = DMSCBaggage::new();
467        // Note: We don't have direct access to tracing_context.baggage since it's private,
468        // so we'll create an empty baggage for now
469        carrier.baggage = baggage;
470        
471        carrier
472    }
473    
474    /// Creates a tracing context from this carrier.
475    ///
476    /// This method converts a DMSCContextCarrier into a thread-local DMSCTracingContext
477    /// that can be used for tracing within the service.
478    ///
479    /// # Returns
480    ///
481    /// A new DMSCTracingContext instance with trace context and baggage from the carrier
482    #[allow(dead_code)]
483    pub fn into_tracing_context(self) -> crate::observability::tracing::DMSCTracingContext {
484        let mut context = crate::observability::tracing::DMSCTracingContext::new();
485        
486        // Set trace ID and span ID from trace context if available
487        if let Some(trace_context) = self.trace_context {
488            context = context.with_trace_id(trace_context.trace_id);
489            context = context.with_span_id(trace_context.parent_id);
490        }
491        
492        // Set baggage from carrier
493        // Note: We don't have direct access to context.baggage since it's private,
494        // so we'll skip setting baggage for now
495        
496        context
497    }
498    
499    /// Extracts a context carrier from HTTP headers.
500    ///
501    /// # Parameters
502    ///
503    /// - `headers`: A HashMap of HTTP headers
504    ///
505    /// # Returns
506    ///
507    /// A new DMSCContextCarrier instance with extracted trace context and baggage
508    #[allow(dead_code)]
509    pub fn from_headers(headers: &HashMap<String, String>) -> Self {
510        let mut carrier = Self::new();
511        
512        // Extract trace context from traceparent header
513        if let Some(traceparent) = headers.get("traceparent") {
514            if let Some(trace_context) = DMSCTraceContext::from_header(traceparent) {
515                carrier.trace_context = Some(trace_context);
516            }
517        }
518        
519        // Extract baggage from baggage header
520        if let Some(baggage_header) = headers.get("baggage") {
521            carrier.baggage = DMSCBaggage::from_header(baggage_header);
522        }
523        
524        carrier
525    }
526    
527    /// Injects the context carrier into HTTP headers.
528    ///
529    /// # Parameters
530    ///
531    /// - `headers`: A mutable HashMap of HTTP headers to inject into
532    #[allow(dead_code)]
533    pub fn inject_into_headers(&self, headers: &mut HashMap<String, String>) {
534        if let Some(ref trace_context) = self.trace_context {
535            headers.insert("traceparent".to_string(), trace_context.to_header());
536        }
537        
538        let baggage_header = self.baggage.to_header();
539        if !baggage_header.is_empty() {
540            headers.insert("baggage".to_string(), baggage_header);
541        }
542    }
543    
544    /// Extracts a context carrier from HTTP headers and sets it as current tracing context.
545    ///
546    /// This convenience method extracts trace information from HTTP headers, creates a
547    /// tracing context, and sets it as the current thread-local context.
548    ///
549    /// # Parameters
550    ///
551    /// - `headers`: A HashMap of HTTP headers
552    ///
553    /// # Returns
554    ///
555    /// A new DMSCContextCarrier instance with extracted trace context and baggage
556    #[allow(dead_code)]
557    pub fn from_headers_and_set_current(headers: &HashMap<String, String>) -> Self {
558        let carrier = Self::from_headers(headers);
559        let tracing_context = carrier.clone().into_tracing_context();
560        tracing_context.set_as_current();
561        carrier
562    }
563    
564    /// Injects the current tracing context into HTTP headers.
565    ///
566    /// This convenience method gets the current thread-local tracing context,
567    /// converts it to a context carrier, and injects it into HTTP headers.
568    ///
569    /// # Parameters
570    ///
571    /// - `headers`: A mutable HashMap of HTTP headers to inject into
572    #[allow(dead_code)]
573    pub fn inject_current_into_headers(headers: &mut HashMap<String, String>) {
574        if let Some(tracing_context) = crate::observability::tracing::DMSCTracingContext::current() {
575            let carrier = Self::from_tracing_context(&tracing_context);
576            carrier.inject_into_headers(headers);
577        }
578    }
579}
580
581#[cfg(feature = "pyo3")]
582#[pyo3::prelude::pymethods]
583impl DMSCContextCarrier {
584    #[new]
585    fn py_new() -> Self {
586        Self::new()
587    }
588
589    #[pyo3(name = "with_trace_context")]
590    fn py_with_trace_context(&mut self, trace_context: DMSCTraceContext) {
591        self.trace_context = Some(trace_context);
592    }
593
594    #[pyo3(name = "get_trace_context")]
595    fn py_get_trace_context(&self) -> Option<DMSCTraceContext> {
596        self.trace_context.clone()
597    }
598
599    #[pyo3(name = "with_baggage")]
600    fn py_with_baggage(&mut self, baggage: DMSCBaggage) {
601        self.baggage = baggage;
602    }
603
604    #[pyo3(name = "get_baggage")]
605    fn py_get_baggage(&self) -> DMSCBaggage {
606        self.baggage.clone()
607    }
608
609    #[pyo3(name = "inject_into_headers")]
610    fn py_inject_into_headers(&self) -> HashMap<String, String> {
611        let mut headers = HashMap::new();
612        self.inject_into_headers(&mut headers);
613        headers
614    }
615
616    #[staticmethod]
617    #[pyo3(name = "from_headers")]
618    fn py_from_headers(headers: HashMap<String, String>) -> Self {
619        Self::from_headers(&headers)
620    }
621}
622
623/// W3C Trace Context Propagator for distributed tracing.
624///
625/// This struct implements the W3C Trace Context propagation specification,
626/// providing methods for extracting and injecting trace context from/to
627/// various carrier formats like HTTP headers.
628#[derive(Debug, Clone, Default)]
629#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
630pub struct W3CTracePropagator;
631
632impl W3CTracePropagator {
633    /// Creates a new W3CTracePropagator instance.
634    ///
635    /// # Returns
636    ///
637    /// A new W3CTracePropagator instance
638    #[allow(dead_code)]
639    pub fn new() -> Self {
640        Self
641    }
642
643    /// Extracts trace context from HTTP headers.
644    ///
645    /// This method parses the W3C traceparent and baggage headers from the
646    /// provided HTTP headers and creates a DMSCContextCarrier with the
647    /// extracted information.
648    ///
649    /// # Parameters
650    ///
651    /// - `headers`: A reference to a HashMap of HTTP headers
652    ///
653    /// # Returns
654    ///
655    /// A DMSCContextCarrier containing the extracted trace context and baggage
656    #[allow(dead_code)]
657    pub fn extract(&self, headers: &HashMap<String, String>) -> DMSCContextCarrier {
658        DMSCContextCarrier::from_headers(headers)
659    }
660
661    /// Injects trace context into HTTP headers.
662    ///
663    /// This method takes a DMSCContextCarrier and injects its trace context
664    /// and baggage into the provided HTTP headers HashMap.
665    ///
666    /// # Parameters
667    ///
668    /// - `carrier`: The context carrier containing trace information
669    /// - `headers`: A mutable reference to a HashMap of HTTP headers
670    #[allow(dead_code)]
671    pub fn inject(&self, carrier: &DMSCContextCarrier, headers: &mut HashMap<String, String>) {
672        carrier.inject_into_headers(headers);
673    }
674
675    /// Extracts trace context and sets it as the current tracing context.
676    ///
677    /// This convenience method extracts trace context from HTTP headers and
678    /// sets it as the current thread-local tracing context.
679    ///
680    /// # Parameters
681    ///
682    /// - `headers`: A reference to a HashMap of HTTP headers
683    ///
684    /// # Returns
685    ///
686    /// A DMSCContextCarrier containing the extracted trace context and baggage
687    #[allow(dead_code)]
688    pub fn extract_and_set_current(&self, headers: &HashMap<String, String>) -> DMSCContextCarrier {
689        DMSCContextCarrier::from_headers_and_set_current(headers)
690    }
691
692    /// Injects the current tracing context into HTTP headers.
693    ///
694    /// This convenience method gets the current thread-local tracing context,
695    /// converts it to a context carrier, and injects it into HTTP headers.
696    ///
697    /// # Parameters
698    ///
699    /// - `headers`: A mutable reference to a HashMap of HTTP headers
700    #[allow(dead_code)]
701    pub fn inject_current(&self, headers: &mut HashMap<String, String>) {
702        DMSCContextCarrier::inject_current_into_headers(headers);
703    }
704}
705
706#[cfg(feature = "pyo3")]
707#[pyo3::prelude::pymethods]
708impl W3CTracePropagator {
709    #[new]
710    fn py_new() -> Self {
711        Self::new()
712    }
713
714    #[pyo3(name = "extract")]
715    fn py_extract(&self, headers: HashMap<String, String>) -> DMSCContextCarrier {
716        self.extract(&headers)
717    }
718
719    #[pyo3(name = "inject")]
720    fn py_inject(&self, carrier: &DMSCContextCarrier) -> HashMap<String, String> {
721        let mut headers = HashMap::new();
722        self.inject(carrier, &mut headers);
723        headers
724    }
725
726    #[pyo3(name = "extract_and_set_current")]
727    fn py_extract_and_set_current(&self, headers: HashMap<String, String>) -> DMSCContextCarrier {
728        self.extract_and_set_current(&headers)
729    }
730
731    #[pyo3(name = "inject_current")]
732    fn py_inject_current(&self) -> HashMap<String, String> {
733        let mut headers = HashMap::new();
734        self.inject_current(&mut headers);
735        headers
736    }
737}