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}