1use anyhow::Result;
20use async_trait::async_trait;
21use serde::{Deserialize, Serialize};
22use std::collections::HashMap;
23use std::sync::Arc;
24use std::time::Instant;
25use tokio::sync::RwLock;
26
27use super::{MetricValue, TelemetryContext, TelemetryMetric, TelemetryProvider};
28
29#[derive(Debug, Clone)]
31pub struct DistributedSpan {
32 pub span_id: String,
34
35 pub trace_id: String,
37
38 pub parent_span_id: Option<String>,
40
41 pub operation_name: String,
43
44 pub start_time: Instant,
46
47 pub end_time: Option<Instant>,
49
50 pub kind: SpanKind,
52
53 pub status: SpanStatus,
55
56 pub attributes: HashMap<String, String>,
58
59 pub events: Vec<SpanEvent>,
61
62 pub links: Vec<SpanLink>,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
68pub enum SpanKind {
69 Internal,
71
72 Server,
74
75 Client,
77
78 Producer,
80
81 Consumer,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct SpanStatus {
88 pub code: StatusCode,
89 pub description: Option<String>,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94pub enum StatusCode {
95 Ok,
97
98 Error,
100
101 Unset,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct SpanEvent {
108 pub name: String,
109 #[serde(skip, default = "Instant::now")]
110 pub timestamp: Instant,
111 pub timestamp_utc: chrono::DateTime<chrono::Utc>,
112 pub attributes: HashMap<String, String>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct SpanLink {
118 pub trace_id: String,
119 pub span_id: String,
120 pub attributes: HashMap<String, String>,
121}
122
123pub struct DistributedTracingProvider {
125 base_provider: Arc<dyn TelemetryProvider>,
127
128 active_spans: Arc<RwLock<HashMap<String, DistributedSpan>>>,
130
131 completed_spans: Arc<RwLock<Vec<DistributedSpan>>>,
133
134 sampler: Arc<dyn TracingSampler>,
136
137 #[allow(dead_code)] propagator: Arc<dyn ContextPropagator>,
140}
141
142#[derive(Debug, Clone)]
144pub struct SamplingDecision {
145 pub sampled: bool,
146 pub attributes: Option<SamplingAttributes>,
147}
148
149#[derive(Debug, Clone)]
151pub struct SamplingAttributes {
152 pub sampling_priority: f64,
153 pub sampling_rate: f64,
154}
155
156pub trait TracingSampler: Send + Sync {
158 fn should_sample(
160 &self,
161 trace_id: &str,
162 parent_context: Option<&TelemetryContext>,
163 span_name: &str,
164 span_kind: SpanKind,
165 attributes: &[(String, String)],
166 ) -> SamplingDecision;
167}
168
169#[async_trait]
171pub trait ContextPropagator: Send + Sync {
172 async fn extract(&self, carrier: &dyn ContextCarrier) -> Result<Option<TelemetryContext>>;
174
175 async fn inject(
177 &self,
178 context: &TelemetryContext,
179 carrier: &mut dyn ContextCarrier,
180 ) -> Result<()>;
181}
182
183pub trait ContextCarrier: Send + Sync {
185 fn get(&self, key: &str) -> Option<&str>;
187
188 fn set(&mut self, key: &str, value: String);
190
191 fn keys(&self) -> Vec<&str>;
193}
194
195impl DistributedTracingProvider {
196 pub fn new(
198 base_provider: Arc<dyn TelemetryProvider>,
199 sampler: Arc<dyn TracingSampler>,
200 propagator: Arc<dyn ContextPropagator>,
201 ) -> Self {
202 Self {
203 base_provider,
204 active_spans: Arc::new(RwLock::new(HashMap::new())),
205 completed_spans: Arc::new(RwLock::new(Vec::with_capacity(1000))),
206 sampler,
207 propagator,
208 }
209 }
210
211 pub async fn start_distributed_span(
213 &self,
214 operation_name: &str,
215 kind: SpanKind,
216 parent_context: Option<&TelemetryContext>,
217 ) -> DistributedSpan {
218 let (trace_id, parent_span_id) = if let Some(parent) = parent_context {
219 (parent.trace_id.clone(), Some(parent.span_id.clone()))
220 } else {
221 (uuid::Uuid::new_v4().to_string(), None)
222 };
223
224 let span_id = uuid::Uuid::new_v4().to_string();
225
226 let sampling_decision =
228 self.sampler
229 .should_sample(&trace_id, parent_context, operation_name, kind, &[]);
230
231 let mut attributes = HashMap::new();
232
233 if let Some(sampling_attrs) = sampling_decision.attributes {
235 attributes.insert(
236 "sampling.priority".to_string(),
237 sampling_attrs.sampling_priority.to_string(),
238 );
239 attributes.insert(
240 "sampling.rate".to_string(),
241 sampling_attrs.sampling_rate.to_string(),
242 );
243 }
244
245 attributes.insert("span.kind".to_string(), format!("{kind:?}"));
247 attributes.insert("service.name".to_string(), "kindly-guard".to_string());
248
249 let span = DistributedSpan {
250 span_id: span_id.clone(),
251 trace_id: trace_id.clone(),
252 parent_span_id,
253 operation_name: operation_name.to_string(),
254 start_time: Instant::now(),
255 end_time: None,
256 kind,
257 status: SpanStatus {
258 code: StatusCode::Unset,
259 description: None,
260 },
261 attributes,
262 events: Vec::new(),
263 links: Vec::new(),
264 };
265
266 if sampling_decision.sampled {
268 self.active_spans
269 .write()
270 .await
271 .insert(span_id.clone(), span.clone());
272
273 self.base_provider.start_span(operation_name);
275 }
276
277 span
278 }
279
280 pub async fn add_span_event(
282 &self,
283 span_id: &str,
284 event_name: &str,
285 attributes: Vec<(&str, &str)>,
286 ) {
287 if let Some(span) = self.active_spans.write().await.get_mut(span_id) {
288 let event = SpanEvent {
289 name: event_name.to_string(),
290 timestamp: Instant::now(),
291 timestamp_utc: chrono::Utc::now(),
292 attributes: attributes
293 .into_iter()
294 .map(|(k, v)| (k.to_string(), v.to_string()))
295 .collect(),
296 };
297 span.events.push(event);
298 }
299 }
300
301 pub async fn add_span_link(
303 &self,
304 span_id: &str,
305 link_trace_id: &str,
306 link_span_id: &str,
307 attributes: Vec<(&str, &str)>,
308 ) {
309 if let Some(span) = self.active_spans.write().await.get_mut(span_id) {
310 let link = SpanLink {
311 trace_id: link_trace_id.to_string(),
312 span_id: link_span_id.to_string(),
313 attributes: attributes
314 .into_iter()
315 .map(|(k, v)| (k.to_string(), v.to_string()))
316 .collect(),
317 };
318 span.links.push(link);
319 }
320 }
321
322 pub async fn end_distributed_span(&self, span_id: &str, status: SpanStatus) {
324 if let Some(mut span) = self.active_spans.write().await.remove(span_id) {
325 span.end_time = Some(Instant::now());
326 span.status = status;
327
328 if let Some(end_time) = span.end_time {
330 let duration = end_time.duration_since(span.start_time);
331
332 self.base_provider.record_metric(TelemetryMetric {
334 name: format!("trace.span.duration.{}", span.operation_name),
335 value: MetricValue::Histogram(duration.as_secs_f64() * 1000.0),
336 labels: vec![
337 ("span.kind".to_string(), format!("{:?}", span.kind)),
338 ("status.code".to_string(), format!("{:?}", span.status.code)),
339 ],
340 });
341 }
342
343 let mut completed = self.completed_spans.write().await;
345 completed.push(span.clone());
346
347 if completed.len() > 10000 {
349 completed.drain(0..1000);
350 }
351 }
352 }
353
354 pub async fn get_span_context(&self, span_id: &str) -> Option<TelemetryContext> {
356 self.active_spans
357 .read()
358 .await
359 .get(span_id)
360 .map(|span| TelemetryContext {
361 trace_id: span.trace_id.clone(),
362 span_id: span.span_id.clone(),
363 parent_span_id: span.parent_span_id.clone(),
364 baggage: vec![],
365 })
366 }
367
368 pub async fn export_spans(&self) -> Vec<DistributedSpan> {
370 let mut completed = self.completed_spans.write().await;
371 let spans: Vec<_> = completed.drain(..).collect();
372 spans
373 }
374}
375
376pub struct ProbabilitySampler {
378 sampling_rate: f64,
379}
380
381impl ProbabilitySampler {
382 pub fn new(sampling_rate: f64) -> Self {
383 Self {
384 sampling_rate: sampling_rate.clamp(0.0, 1.0),
385 }
386 }
387}
388
389impl TracingSampler for ProbabilitySampler {
390 fn should_sample(
391 &self,
392 trace_id: &str,
393 parent_context: Option<&TelemetryContext>,
394 _span_name: &str,
395 _span_kind: SpanKind,
396 _attributes: &[(String, String)],
397 ) -> SamplingDecision {
398 if parent_context.is_some() {
400 return SamplingDecision {
401 sampled: true,
402 attributes: Some(SamplingAttributes {
403 sampling_priority: 1.0,
404 sampling_rate: self.sampling_rate,
405 }),
406 };
407 }
408
409 use std::collections::hash_map::DefaultHasher;
412 use std::hash::{Hash, Hasher};
413
414 let mut hasher = DefaultHasher::new();
415 trace_id.hash(&mut hasher);
416 let hash = hasher.finish();
417
418 let probability = (hash as f64) / (u64::MAX as f64);
419 let sampled = probability < self.sampling_rate;
420
421 SamplingDecision {
422 sampled,
423 attributes: if sampled {
424 Some(SamplingAttributes {
425 sampling_priority: if sampled { 1.0 } else { 0.0 },
426 sampling_rate: self.sampling_rate,
427 })
428 } else {
429 None
430 },
431 }
432 }
433}
434
435pub struct W3CTraceContextPropagator;
437
438#[async_trait]
439impl ContextPropagator for W3CTraceContextPropagator {
440 async fn extract(&self, carrier: &dyn ContextCarrier) -> Result<Option<TelemetryContext>> {
441 if let Some(traceparent) = carrier.get("traceparent") {
443 let parts: Vec<&str> = traceparent.split('-').collect();
445 if parts.len() >= 4 {
446 let trace_id = parts[1].to_string();
447 let span_id = parts[2].to_string();
448
449 let baggage = if let Some(tracestate) = carrier.get("tracestate") {
451 tracestate
452 .split(',')
453 .filter_map(|kv| {
454 let parts: Vec<&str> = kv.split('=').collect();
455 if parts.len() == 2 {
456 Some((parts[0].to_string(), parts[1].to_string()))
457 } else {
458 None
459 }
460 })
461 .collect()
462 } else {
463 vec![]
464 };
465
466 return Ok(Some(TelemetryContext {
467 trace_id,
468 span_id: uuid::Uuid::new_v4().to_string(), parent_span_id: Some(span_id),
470 baggage,
471 }));
472 }
473 }
474
475 Ok(None)
476 }
477
478 async fn inject(
479 &self,
480 context: &TelemetryContext,
481 carrier: &mut dyn ContextCarrier,
482 ) -> Result<()> {
483 let parent_id = context.parent_span_id.as_ref().unwrap_or(&context.span_id);
485 let traceparent = format!("00-{}-{}-01", context.trace_id, parent_id);
486 carrier.set("traceparent", traceparent);
487
488 if !context.baggage.is_empty() {
490 let tracestate = context
491 .baggage
492 .iter()
493 .map(|(k, v)| format!("{k}={v}"))
494 .collect::<Vec<_>>()
495 .join(",");
496 carrier.set("tracestate", tracestate);
497 }
498
499 Ok(())
500 }
501}
502
503pub struct HttpHeadersCarrier {
505 headers: HashMap<String, String>,
506}
507
508impl Default for HttpHeadersCarrier {
509 fn default() -> Self {
510 Self::new()
511 }
512}
513
514impl HttpHeadersCarrier {
515 pub fn new() -> Self {
516 Self {
517 headers: HashMap::new(),
518 }
519 }
520
521 pub const fn from_headers(headers: HashMap<String, String>) -> Self {
522 Self { headers }
523 }
524}
525
526impl ContextCarrier for HttpHeadersCarrier {
527 fn get(&self, key: &str) -> Option<&str> {
528 self.headers.get(key).map(std::string::String::as_str)
529 }
530
531 fn set(&mut self, key: &str, value: String) {
532 self.headers.insert(key.to_string(), value);
533 }
534
535 fn keys(&self) -> Vec<&str> {
536 self.headers
537 .keys()
538 .map(std::string::String::as_str)
539 .collect()
540 }
541}
542
543pub struct SpanBuilder<'a> {
545 provider: &'a DistributedTracingProvider,
546 operation_name: String,
547 kind: SpanKind,
548 parent_context: Option<TelemetryContext>,
549 attributes: Vec<(String, String)>,
550 links: Vec<SpanLink>,
551}
552
553impl<'a> SpanBuilder<'a> {
554 pub fn new(provider: &'a DistributedTracingProvider, operation_name: &str) -> Self {
555 Self {
556 provider,
557 operation_name: operation_name.to_string(),
558 kind: SpanKind::Internal,
559 parent_context: None,
560 attributes: Vec::new(),
561 links: Vec::new(),
562 }
563 }
564
565 pub const fn with_kind(mut self, kind: SpanKind) -> Self {
566 self.kind = kind;
567 self
568 }
569
570 pub fn with_parent(mut self, parent: TelemetryContext) -> Self {
571 self.parent_context = Some(parent);
572 self
573 }
574
575 pub fn with_attribute(mut self, key: &str, value: &str) -> Self {
576 self.attributes.push((key.to_string(), value.to_string()));
577 self
578 }
579
580 pub fn with_link(mut self, trace_id: &str, span_id: &str) -> Self {
581 self.links.push(SpanLink {
582 trace_id: trace_id.to_string(),
583 span_id: span_id.to_string(),
584 attributes: HashMap::new(),
585 });
586 self
587 }
588
589 pub async fn start(self) -> DistributedSpan {
590 let mut span = self
591 .provider
592 .start_distributed_span(
593 &self.operation_name,
594 self.kind,
595 self.parent_context.as_ref(),
596 )
597 .await;
598
599 for (key, value) in self.attributes {
601 span.attributes.insert(key, value);
602 }
603
604 span.links = self.links;
606
607 if let Some(existing_span) = self
609 .provider
610 .active_spans
611 .write()
612 .await
613 .get_mut(&span.span_id)
614 {
615 existing_span.attributes = span.attributes.clone();
616 existing_span.links = span.links.clone();
617 }
618
619 span
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626 use crate::telemetry::standard::StandardTelemetryProvider;
627 use crate::telemetry::TelemetryConfig;
628
629 #[tokio::test]
630 async fn test_distributed_span_lifecycle() {
631 let config = TelemetryConfig::default();
632 let base_provider = Arc::new(StandardTelemetryProvider::new(config));
633 let sampler = Arc::new(ProbabilitySampler::new(1.0)); let propagator = Arc::new(W3CTraceContextPropagator);
635
636 let provider = DistributedTracingProvider::new(base_provider, sampler, propagator);
637
638 let span = provider
640 .start_distributed_span("test_operation", SpanKind::Internal, None)
641 .await;
642
643 assert!(!span.trace_id.is_empty());
644 assert!(!span.span_id.is_empty());
645 assert_eq!(span.operation_name, "test_operation");
646
647 provider
649 .add_span_event(&span.span_id, "test_event", vec![("key", "value")])
650 .await;
651
652 provider
654 .end_distributed_span(
655 &span.span_id,
656 SpanStatus {
657 code: StatusCode::Ok,
658 description: None,
659 },
660 )
661 .await;
662
663 let exported = provider.export_spans().await;
665 assert_eq!(exported.len(), 1);
666 assert_eq!(exported[0].span_id, span.span_id);
667 }
668
669 #[tokio::test]
670 async fn test_context_propagation() {
671 let propagator = W3CTraceContextPropagator;
672 let mut carrier = HttpHeadersCarrier::new();
673
674 let context = TelemetryContext {
675 trace_id: "00112233445566778899aabbccddeeff".to_string(),
676 span_id: "0123456789abcdef".to_string(),
677 parent_span_id: Some("fedcba9876543210".to_string()),
678 baggage: vec![("user".to_string(), "test".to_string())],
679 };
680
681 propagator.inject(&context, &mut carrier).await.unwrap();
683 assert!(carrier.get("traceparent").is_some());
684
685 let extracted = propagator.extract(&carrier).await.unwrap().unwrap();
687 assert_eq!(extracted.trace_id, context.trace_id);
688 assert_eq!(
689 extracted.parent_span_id,
690 Some("fedcba9876543210".to_string())
691 );
692 }
693
694 #[test]
695 fn test_probability_sampler() {
696 let sampler = ProbabilitySampler::new(0.5);
697
698 let sampled_count = (0..1000)
700 .filter(|i| {
701 let trace_id = format!("trace-{}", i);
702 let decision =
703 sampler.should_sample(&trace_id, None, "test", SpanKind::Internal, &[]);
704 decision.sampled
705 })
706 .count();
707
708 assert!(
711 sampled_count > 350 && sampled_count < 650,
712 "Sampled count {} is outside expected range [350, 650]",
713 sampled_count
714 );
715 }
716}