1use std::collections::HashMap;
14use std::sync::{Arc, Mutex};
15use std::time::{SystemTime, UNIX_EPOCH};
16
17use super::types::{FieldValue, SpanContext, TraceConfig};
18
19struct LcgId {
25 state: u64,
26}
27
28impl LcgId {
29 fn new() -> Self {
30 let seed = SystemTime::now()
31 .duration_since(UNIX_EPOCH)
32 .map(|d| d.as_nanos() as u64)
33 .unwrap_or(12345)
34 ^ 0xdeadbeef_cafebabe;
35 Self { state: seed }
36 }
37
38 fn next(&mut self) -> u64 {
39 self.state = self
41 .state
42 .wrapping_mul(6_364_136_223_846_793_005)
43 .wrapping_add(1_442_695_040_888_963_407);
44 if self.state == 0 {
45 self.state = 1;
46 }
47 self.state
48 }
49}
50
51fn next_id() -> u64 {
53 use std::cell::RefCell;
54 thread_local! {
55 static GEN: RefCell<LcgId> = RefCell::new(LcgId::new());
56 }
57 GEN.with(|g| g.borrow_mut().next())
58}
59
60#[non_exhaustive]
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum SpanStatus {
68 Unset,
70 Ok,
72 Error(String),
74}
75
76impl Default for SpanStatus {
77 fn default() -> Self {
78 SpanStatus::Unset
79 }
80}
81
82#[derive(Debug, Clone)]
88pub struct SpanEvent {
89 pub name: String,
91 pub timestamp_ns: u64,
93 pub attributes: Vec<(String, FieldValue)>,
95}
96
97impl SpanEvent {
98 fn new(name: impl Into<String>, attributes: Vec<(String, FieldValue)>) -> Self {
99 let timestamp_ns = SystemTime::now()
100 .duration_since(UNIX_EPOCH)
101 .map(|d| d.as_nanos() as u64)
102 .unwrap_or(0);
103 Self {
104 name: name.into(),
105 timestamp_ns,
106 attributes,
107 }
108 }
109}
110
111#[derive(Debug, Clone)]
120pub struct Span {
121 pub context: SpanContext,
123 pub name: String,
125 pub start_ns: u64,
127 pub end_ns: Option<u64>,
129 pub events: Vec<SpanEvent>,
131 pub attributes: Vec<(String, FieldValue)>,
133 pub status: SpanStatus,
135 pub(crate) sampled: bool,
137}
138
139impl Span {
140 pub fn set_attribute(&mut self, key: impl Into<String>, value: FieldValue) {
142 let key = key.into();
143 if let Some(entry) = self.attributes.iter_mut().find(|(k, _)| k == &key) {
144 entry.1 = value;
145 } else {
146 self.attributes.push((key, value));
147 }
148 }
149
150 pub fn add_event(&mut self, name: impl Into<String>, attrs: Vec<(String, FieldValue)>) {
152 self.events.push(SpanEvent::new(name, attrs));
153 }
154
155 pub fn set_status(&mut self, status: SpanStatus) {
157 self.status = status;
158 }
159
160 pub fn end(&mut self) {
162 self.end_ns = Some(
163 SystemTime::now()
164 .duration_since(UNIX_EPOCH)
165 .map(|d| d.as_nanos() as u64)
166 .unwrap_or(0),
167 );
168 }
169
170 pub fn duration_ns(&self) -> Option<u64> {
173 self.end_ns.map(|e| e.saturating_sub(self.start_ns))
174 }
175}
176
177pub trait Sampler: Send + Sync {
183 fn should_sample(&self, trace_id: u64, name: &str) -> bool;
186}
187
188pub struct AlwaysOnSampler;
190
191impl Sampler for AlwaysOnSampler {
192 fn should_sample(&self, _trace_id: u64, _name: &str) -> bool {
193 true
194 }
195}
196
197pub struct AlwaysOffSampler;
199
200impl Sampler for AlwaysOffSampler {
201 fn should_sample(&self, _trace_id: u64, _name: &str) -> bool {
202 false
203 }
204}
205
206pub struct TraceIdRatioSampler {
211 ratio: f64,
213}
214
215impl TraceIdRatioSampler {
216 pub fn new(ratio: f64) -> Self {
220 Self {
221 ratio: ratio.clamp(0.0, 1.0),
222 }
223 }
224}
225
226impl Sampler for TraceIdRatioSampler {
227 fn should_sample(&self, trace_id: u64, _name: &str) -> bool {
228 if self.ratio >= 1.0 {
229 return true;
230 }
231 if self.ratio <= 0.0 {
232 return false;
233 }
234 let mut h = trace_id;
237 h ^= h >> 33;
238 h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
239 h ^= h >> 33;
240 h = h.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
241 h ^= h >> 33;
242 let normalised = (h as f64) / (u64::MAX as f64 + 1.0);
244 normalised < self.ratio
245 }
246}
247
248pub struct ParentBasedSampler {
250 root_sampler: Box<dyn Sampler>,
251}
252
253impl ParentBasedSampler {
254 pub fn new(root_sampler: Box<dyn Sampler>) -> Self {
256 Self { root_sampler }
257 }
258}
259
260impl Sampler for ParentBasedSampler {
261 fn should_sample(&self, trace_id: u64, name: &str) -> bool {
262 self.root_sampler.should_sample(trace_id, name)
264 }
265}
266
267impl ParentBasedSampler {
269 pub fn should_sample_with_parent(
272 &self,
273 parent_sampled: Option<bool>,
274 trace_id: u64,
275 name: &str,
276 ) -> bool {
277 match parent_sampled {
278 Some(decision) => decision,
279 None => self.root_sampler.should_sample(trace_id, name),
280 }
281 }
282}
283
284pub trait SpanExporter: Send + Sync {
290 fn export(&self, spans: Vec<Span>);
292}
293
294pub struct InMemoryExporter {
296 spans: Mutex<Vec<Span>>,
297}
298
299impl InMemoryExporter {
300 pub fn new() -> Self {
302 Self {
303 spans: Mutex::new(Vec::new()),
304 }
305 }
306
307 pub fn spans(&self) -> Vec<Span> {
309 self.spans.lock().map(|g| g.clone()).unwrap_or_default()
310 }
311
312 pub fn clear(&self) {
314 if let Ok(mut g) = self.spans.lock() {
315 g.clear();
316 }
317 }
318}
319
320impl Default for InMemoryExporter {
321 fn default() -> Self {
322 Self::new()
323 }
324}
325
326impl SpanExporter for InMemoryExporter {
327 fn export(&self, spans: Vec<Span>) {
328 if let Ok(mut g) = self.spans.lock() {
329 g.extend(spans);
330 }
331 }
332}
333
334#[derive(Debug, Clone)]
342pub struct OtlpSpan {
343 pub trace_id: String,
345 pub span_id: String,
347 pub parent_span_id: String,
349 pub name: String,
351 pub start_time_unix_nano: u64,
353 pub end_time_unix_nano: u64,
355 pub attributes: Vec<(String, String)>,
357 pub status_code: String,
359 pub status_message: String,
361}
362
363pub struct OtlpStubExporter {
365 spans: Mutex<Vec<OtlpSpan>>,
366}
367
368impl OtlpStubExporter {
369 pub fn new() -> Self {
371 Self {
372 spans: Mutex::new(Vec::new()),
373 }
374 }
375
376 pub fn to_otlp(span: &Span) -> OtlpSpan {
378 let trace_hex = format!("{:016x}{:016x}", 0u64, span.context.trace_id);
379 let span_hex = format!("{:016x}", span.context.span_id);
380 let parent_hex = span
381 .context
382 .parent_id
383 .map(|p| format!("{:016x}", p))
384 .unwrap_or_default();
385
386 let attributes: Vec<(String, String)> = span
387 .attributes
388 .iter()
389 .map(|(k, v)| (k.clone(), v.to_json_value()))
390 .collect();
391
392 let (status_code, status_message) = match &span.status {
393 SpanStatus::Unset => ("UNSET".to_owned(), String::new()),
394 SpanStatus::Ok => ("OK".to_owned(), String::new()),
395 SpanStatus::Error(msg) => ("ERROR".to_owned(), msg.clone()),
396 };
397
398 OtlpSpan {
399 trace_id: trace_hex,
400 span_id: span_hex,
401 parent_span_id: parent_hex,
402 name: span.name.clone(),
403 start_time_unix_nano: span.start_ns,
404 end_time_unix_nano: span.end_ns.unwrap_or(span.start_ns),
405 attributes,
406 status_code,
407 status_message,
408 }
409 }
410
411 pub fn otlp_spans(&self) -> Vec<OtlpSpan> {
413 self.spans.lock().map(|g| g.clone()).unwrap_or_default()
414 }
415}
416
417impl Default for OtlpStubExporter {
418 fn default() -> Self {
419 Self::new()
420 }
421}
422
423impl SpanExporter for OtlpStubExporter {
424 fn export(&self, spans: Vec<Span>) {
425 let otlp: Vec<OtlpSpan> = spans.iter().map(Self::to_otlp).collect();
426 if let Ok(mut g) = self.spans.lock() {
427 g.extend(otlp);
428 }
429 }
430}
431
432pub struct TraceContext;
440
441impl TraceContext {
442 pub fn inject(context: &SpanContext, headers: &mut Vec<(String, String)>) {
446 let traceparent = format!(
448 "00-{:016x}{:016x}-{:016x}-01",
449 0u64, context.trace_id, context.span_id
450 );
451 headers.retain(|(k, _)| k.to_lowercase() != "traceparent");
453 headers.push(("traceparent".to_owned(), traceparent));
454
455 if !context.baggage.is_empty() {
457 let baggage: Vec<String> = context
458 .baggage
459 .iter()
460 .map(|(k, v)| format!("{}={}", k, v))
461 .collect();
462 headers.retain(|(k, _)| k.to_lowercase() != "baggage");
463 headers.push(("baggage".to_owned(), baggage.join(",")));
464 }
465 }
466
467 pub fn extract(headers: &[(String, String)]) -> Option<SpanContext> {
471 let traceparent = headers
472 .iter()
473 .find(|(k, _)| k.to_lowercase() == "traceparent")
474 .map(|(_, v)| v.as_str())?;
475
476 let parts: Vec<&str> = traceparent.split('-').collect();
478 if parts.len() != 4 {
479 return None;
480 }
481 let trace_hex = parts[1];
483 if trace_hex.len() != 32 {
484 return None;
485 }
486 let trace_id = u64::from_str_radix(&trace_hex[16..], 16).ok()?;
487
488 let span_hex = parts[2];
489 if span_hex.len() != 16 {
490 return None;
491 }
492 let span_id = u64::from_str_radix(span_hex, 16).ok()?;
493
494 Some(SpanContext {
495 trace_id,
496 span_id,
497 parent_id: None,
498 baggage: Vec::new(),
499 })
500 }
501}
502
503pub struct Tracer {
509 config: TraceConfig,
510 sampler: Box<dyn Sampler>,
511 exporter: Arc<dyn SpanExporter>,
512 active_spans: Arc<Mutex<HashMap<u64, Span>>>,
514}
515
516impl Tracer {
517 pub fn new(config: TraceConfig, exporter: Arc<dyn SpanExporter>) -> Self {
519 Self {
520 sampler: Box::new(AlwaysOnSampler),
521 config,
522 exporter,
523 active_spans: Arc::new(Mutex::new(HashMap::new())),
524 }
525 }
526
527 pub fn with_sampler(mut self, sampler: Box<dyn Sampler>) -> Self {
529 self.sampler = sampler;
530 self
531 }
532
533 pub fn start_span(&self, name: &str, parent: Option<&SpanContext>) -> Span {
535 let (trace_id, parent_id) = match parent {
536 Some(p) => (p.trace_id, Some(p.span_id)),
537 None => (next_id(), None),
538 };
539 let span_id = next_id();
540
541 let sampled = self.sampler.should_sample(trace_id, name);
542
543 let context = SpanContext {
544 trace_id,
545 span_id,
546 parent_id,
547 baggage: parent.map(|p| p.baggage.clone()).unwrap_or_default(),
548 };
549
550 let start_ns = SystemTime::now()
551 .duration_since(UNIX_EPOCH)
552 .map(|d| d.as_nanos() as u64)
553 .unwrap_or(0);
554
555 let span = Span {
556 context,
557 name: name.to_owned(),
558 start_ns,
559 end_ns: None,
560 events: Vec::new(),
561 attributes: Vec::new(),
562 status: SpanStatus::default(),
563 sampled,
564 };
565
566 if sampled {
567 if let Ok(mut active) = self.active_spans.lock() {
568 if active.len() >= self.config.max_spans {
570 if let Some(&oldest_id) = active
572 .iter()
573 .min_by_key(|(_, s)| s.start_ns)
574 .map(|(id, _)| id)
575 {
576 active.remove(&oldest_id);
577 }
578 }
579 active.insert(span_id, span.clone());
580 }
581 }
582
583 span
584 }
585
586 pub fn finish_span(&self, mut span: Span) {
588 if span.end_ns.is_none() {
589 span.end();
590 }
591 if !span.sampled {
592 return;
593 }
594 if let Ok(mut active) = self.active_spans.lock() {
596 active.remove(&span.context.span_id);
597 }
598 self.exporter.export(vec![span]);
599 }
600
601 pub fn with_span<F, R>(&self, name: &str, f: F) -> R
603 where
604 F: FnOnce(&mut Span) -> R,
605 {
606 let mut span = self.start_span(name, None);
607 let result = f(&mut span);
608 self.finish_span(span);
609 result
610 }
611
612 pub fn with_child_span<F, R>(&self, name: &str, parent: &SpanContext, f: F) -> R
614 where
615 F: FnOnce(&mut Span) -> R,
616 {
617 let mut span = self.start_span(name, Some(parent));
618 let result = f(&mut span);
619 self.finish_span(span);
620 result
621 }
622}
623
624#[cfg(test)]
629mod tests {
630 use super::*;
631
632 fn make_tracer() -> (Tracer, Arc<InMemoryExporter>) {
633 let exporter = Arc::new(InMemoryExporter::new());
634 let tracer = Tracer::new(
635 TraceConfig::default(),
636 Arc::clone(&exporter) as Arc<dyn SpanExporter>,
637 );
638 (tracer, exporter)
639 }
640
641 #[test]
642 fn test_span_lifecycle() {
643 let (tracer, exporter) = make_tracer();
644 tracer.with_span("op", |span| {
645 span.add_event("retry", vec![]);
646 span.set_attribute("host", FieldValue::Str("localhost".into()));
647 });
648 let spans = exporter.spans();
649 assert_eq!(spans.len(), 1);
650 assert_eq!(spans[0].name, "op");
651 assert_eq!(spans[0].events.len(), 1);
652 assert_eq!(spans[0].attributes.len(), 1);
653 assert!(spans[0].end_ns.is_some());
654 }
655
656 #[test]
657 fn test_span_parent_child() {
658 let (tracer, exporter) = make_tracer();
659 let parent = tracer.start_span("parent", None);
660 let parent_ctx = parent.context.clone();
661 tracer.finish_span(parent);
662
663 let child = tracer.start_span("child", Some(&parent_ctx));
664 assert_eq!(child.context.trace_id, parent_ctx.trace_id);
665 assert_eq!(child.context.parent_id, Some(parent_ctx.span_id));
666 tracer.finish_span(child);
667
668 let spans = exporter.spans();
669 assert_eq!(spans.len(), 2);
670 }
671
672 #[test]
673 fn test_tracer_with_span() {
674 let (tracer, exporter) = make_tracer();
675 let val = tracer.with_span("compute", |_span| 42);
676 assert_eq!(val, 42);
677 assert_eq!(exporter.spans().len(), 1);
678 }
679
680 #[test]
681 fn test_in_memory_exporter() {
682 let exporter = InMemoryExporter::new();
683 let span = Span {
684 context: SpanContext::root(1, 2),
685 name: "test".into(),
686 start_ns: 0,
687 end_ns: Some(100),
688 events: vec![],
689 attributes: vec![],
690 status: SpanStatus::Ok,
691 sampled: true,
692 };
693 exporter.export(vec![span]);
694 assert_eq!(exporter.spans().len(), 1);
695 }
696
697 #[test]
698 fn test_always_on_sampler() {
699 let s = AlwaysOnSampler;
700 for i in 0..100u64 {
701 assert!(s.should_sample(i, "op"));
702 }
703 }
704
705 #[test]
706 fn test_ratio_sampler_approx() {
707 let s = TraceIdRatioSampler::new(0.5);
708 let sampled = (0u64..10_000)
709 .filter(|&id| s.should_sample(id, "op"))
710 .count();
711 assert!(sampled > 4000, "sampled={}", sampled);
713 assert!(sampled < 6000, "sampled={}", sampled);
714 }
715
716 #[test]
717 fn test_parent_based_sampler_follows() {
718 let s = ParentBasedSampler::new(Box::new(AlwaysOffSampler));
719 assert!(s.should_sample_with_parent(Some(true), 0, "op"));
721 assert!(!s.should_sample_with_parent(Some(false), 0, "op"));
723 assert!(!s.should_sample_with_parent(None, 0, "op"));
725 }
726
727 #[test]
728 fn test_traceparent_inject() {
729 let ctx = SpanContext::root(0xdeadbeef, 0xcafe);
730 let mut headers = Vec::new();
731 TraceContext::inject(&ctx, &mut headers);
732 let tp = headers
733 .iter()
734 .find(|(k, _)| k == "traceparent")
735 .map(|(_, v)| v.as_str())
736 .expect("traceparent header missing");
737 assert!(tp.starts_with("00-"));
739 assert!(tp.ends_with("-01"));
740 let parts: Vec<&str> = tp.split('-').collect();
741 assert_eq!(parts.len(), 4);
742 assert_eq!(parts[1].len(), 32);
743 assert_eq!(parts[2].len(), 16);
744 }
745
746 #[test]
747 fn test_traceparent_extract() {
748 let ctx = SpanContext::root(0xdeadbeef, 0xcafe);
749 let mut headers = Vec::new();
750 TraceContext::inject(&ctx, &mut headers);
751 let extracted = TraceContext::extract(&headers).expect("extraction failed");
752 assert_eq!(extracted.trace_id, ctx.trace_id);
753 assert_eq!(extracted.span_id, ctx.span_id);
754 }
755
756 #[test]
757 fn test_span_status_error() {
758 let (tracer, exporter) = make_tracer();
759 tracer.with_span("failing", |span| {
760 span.set_status(SpanStatus::Error("timeout".into()));
761 });
762 let spans = exporter.spans();
763 assert_eq!(spans.len(), 1);
764 match &spans[0].status {
765 SpanStatus::Error(msg) => assert_eq!(msg, "timeout"),
766 other => panic!("expected Error, got {:?}", other),
767 }
768 }
769}