1pub mod sampling;
18
19use chrono::{DateTime, Utc};
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22use std::sync::{Arc, RwLock};
23use std::time::Duration;
24
25pub use sampling::{
26 AlwaysOffSampler, AlwaysOnSampler, Baggage, BaggagePropagator, BatchConfig, BatchSpanExporter,
27 ParentBasedSampler, Sampler, SamplingDecision, TraceIdRatioSampler,
28};
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct Span {
32 pub trace_id: String,
33 pub span_id: String,
34 pub parent_id: Option<String>,
35 pub operation_name: String,
36 pub service_name: String,
37 pub start_time: i64,
38 pub end_time: Option<i64>,
39 pub tags: HashMap<String, String>,
40 pub logs: Vec<SpanLog>,
41}
42
43impl Span {
44 pub fn new(
45 trace_id: impl Into<String>,
46 span_id: impl Into<String>,
47 operation_name: impl Into<String>,
48 ) -> Self {
49 Self {
50 trace_id: trace_id.into(),
51 span_id: span_id.into(),
52 parent_id: None,
53 operation_name: operation_name.into(),
54 service_name: String::new(),
55 start_time: current_timestamp(),
56 end_time: None,
57 tags: HashMap::new(),
58 logs: Vec::new(),
59 }
60 }
61
62 pub fn with_parent(mut self, parent_id: impl Into<String>) -> Self {
63 self.parent_id = Some(parent_id.into());
64 self
65 }
66
67 pub fn with_service(mut self, service_name: impl Into<String>) -> Self {
68 self.service_name = service_name.into();
69 self
70 }
71
72 pub fn with_tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
73 self.tags.insert(key.into(), value.into());
74 self
75 }
76
77 pub fn finish(&mut self) {
78 self.end_time = Some(current_timestamp());
79 }
80
81 pub fn duration(&self) -> Option<i64> {
82 self.end_time.map(|end| end - self.start_time)
83 }
84
85 pub fn add_log(&mut self, message: impl Into<String>) {
86 self.logs.push(SpanLog {
87 timestamp: current_timestamp(),
88 message: message.into(),
89 fields: HashMap::new(),
90 });
91 }
92
93 pub fn trace_id(&self) -> &str {
94 &self.trace_id
95 }
96
97 pub fn span_id(&self) -> &str {
98 &self.span_id
99 }
100
101 pub fn parent_id(&self) -> Option<&str> {
102 self.parent_id.as_deref()
103 }
104
105 pub fn operation_name(&self) -> &str {
106 &self.operation_name
107 }
108
109 pub fn service_name(&self) -> &str {
110 &self.service_name
111 }
112
113 pub fn tags(&self) -> &HashMap<String, String> {
114 &self.tags
115 }
116
117 pub fn logs(&self) -> &[SpanLog] {
118 &self.logs
119 }
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct SpanLog {
124 pub timestamp: i64,
125 pub message: String,
126 pub fields: HashMap<String, String>,
127}
128
129pub trait Tracer: Send + Sync {
130 fn start_span(&self, operation_name: &str) -> Span;
131 fn end_span(&self, span: Span);
132 fn inject(&self, span: &Span) -> HashMap<String, String>;
133 fn extract(&self, headers: &HashMap<String, String>) -> Option<Span>;
134}
135
136pub struct SzTracer {
137 spans: Arc<RwLock<Vec<Span>>>,
138 service_name: String,
139}
140
141impl SzTracer {
142 pub fn new(service_name: impl Into<String>) -> Self {
143 Self {
144 spans: Arc::new(RwLock::new(Vec::new())),
145 service_name: service_name.into(),
146 }
147 }
148
149 pub fn generate_trace_id() -> String {
150 format!("{:032x}", rand_u64())
151 }
152
153 pub fn generate_span_id() -> String {
154 format!("{:016x}", rand_u64())
155 }
156
157 pub fn get_spans(&self) -> Vec<Span> {
158 self.spans
159 .read()
160 .map_err(|e| TracingError::Internal(e.to_string()))
161 .unwrap()
162 .clone()
163 }
164
165 pub fn clear(&self) {
166 self.spans
167 .write()
168 .map_err(|e| TracingError::Internal(e.to_string()))
169 .unwrap()
170 .clear();
171 }
172}
173
174impl Default for SzTracer {
175 fn default() -> Self {
176 Self::new("unknown")
177 }
178}
179
180impl Tracer for SzTracer {
181 fn start_span(&self, operation_name: &str) -> Span {
182 Span::new(
183 Self::generate_trace_id(),
184 Self::generate_span_id(),
185 operation_name,
186 )
187 .with_service(&self.service_name)
188 }
189
190 fn end_span(&self, mut span: Span) {
191 span.finish();
192
193 if let Ok(mut spans) = self.spans.write() {
194 spans.push(span);
195 }
196 }
197
198 fn inject(&self, span: &Span) -> HashMap<String, String> {
211 let mut headers = HashMap::new();
212
213 let traceparent = format!("00-{}-{}-01", span.trace_id, span.span_id);
215 headers.insert("traceparent".to_string(), traceparent);
216
217 if let Some(ref parent_id) = span.parent_id {
218 headers.insert("parent-span-id".to_string(), parent_id.clone());
219 }
220
221 headers
222 }
223
224 fn extract(&self, headers: &HashMap<String, String>) -> Option<Span> {
235 if let Some(traceparent) = headers.get("traceparent") {
237 if let Some(span) = Self::parse_traceparent(traceparent) {
238 let mut span = span.with_service(&self.service_name);
239
240 if let Some(parent_id) = headers.get("parent-span-id") {
242 span = span.with_parent(parent_id.clone());
243 }
244
245 return Some(span);
246 }
247 }
248
249 let trace_id = headers.get("trace-id")?;
251 let span_id = headers.get("span-id")?;
252
253 let mut span = Span::new(trace_id.clone(), span_id.clone(), "extracted");
254
255 if let Some(parent_id) = headers.get("parent-span-id") {
256 span = span.with_parent(parent_id.clone());
257 }
258
259 span = span.with_service(&self.service_name);
260
261 Some(span)
262 }
263}
264
265impl SzTracer {
266 fn parse_traceparent(traceparent: &str) -> Option<Span> {
276 let parts: Vec<&str> = traceparent.split('-').collect();
277 if parts.len() != 4 {
278 return None;
279 }
280
281 let version = parts[0];
282 let trace_id = parts[1];
283 let span_id = parts[2];
284 let trace_flags = parts[3];
285
286 if version.len() != 2 || !version.chars().all(|c| c.is_ascii_hexdigit()) {
288 return None;
289 }
290
291 if trace_id.len() != 32
293 || !trace_id.chars().all(|c| c.is_ascii_hexdigit())
294 || trace_id.chars().all(|c| c == '0')
295 {
296 return None;
297 }
298
299 if span_id.len() != 16
301 || !span_id.chars().all(|c| c.is_ascii_hexdigit())
302 || span_id.chars().all(|c| c == '0')
303 {
304 return None;
305 }
306
307 if trace_flags.len() != 2 || !trace_flags.chars().all(|c| c.is_ascii_hexdigit()) {
309 return None;
310 }
311
312 Some(Span::new(
313 trace_id.to_string(),
314 span_id.to_string(),
315 "extracted",
316 ))
317 }
318
319 pub fn inject_legacy(&self, span: &Span) -> HashMap<String, String> {
324 let mut headers = HashMap::new();
325 headers.insert("trace-id".to_string(), span.trace_id.to_string());
326 headers.insert("span-id".to_string(), span.span_id.to_string());
327
328 if let Some(ref parent_id) = span.parent_id {
329 headers.insert("parent-span-id".to_string(), parent_id.clone());
330 }
331
332 headers
333 }
334
335 pub fn extract_legacy(&self, headers: &HashMap<String, String>) -> Option<Span> {
340 let trace_id = headers.get("trace-id")?;
341 let span_id = headers.get("span-id")?;
342
343 let mut span = Span::new(trace_id.clone(), span_id.clone(), "extracted");
344
345 if let Some(parent_id) = headers.get("parent-span-id") {
346 span = span.with_parent(parent_id.clone());
347 }
348
349 span = span.with_service(&self.service_name);
350
351 Some(span)
352 }
353}
354
355pub struct OtelTracer {
389 tracer: SzTracer,
390}
391
392impl OtelTracer {
393 pub fn new(service_name: impl Into<String>) -> Self {
395 Self {
396 tracer: SzTracer::new(service_name),
397 }
398 }
399
400 pub fn inner(&self) -> &SzTracer {
403 &self.tracer
404 }
405}
406
407impl Tracer for OtelTracer {
408 fn start_span(&self, operation_name: &str) -> Span {
409 self.tracer.start_span(operation_name)
410 }
411
412 fn end_span(&self, span: Span) {
413 self.tracer.end_span(span)
414 }
415
416 fn inject(&self, span: &Span) -> HashMap<String, String> {
417 self.tracer.inject(span)
418 }
419
420 fn extract(&self, headers: &HashMap<String, String>) -> Option<Span> {
421 self.tracer.extract(headers)
422 }
423}
424
425fn current_timestamp() -> i64 {
426 use std::time::{SystemTime, UNIX_EPOCH};
427 SystemTime::now()
428 .duration_since(UNIX_EPOCH)
429 .unwrap_or_default()
430 .as_millis() as i64
431}
432
433fn rand_u64() -> u64 {
434 use std::collections::hash_map::RandomState;
435 use std::hash::{BuildHasher, Hasher};
436 RandomState::new().build_hasher().finish()
437}
438
439#[derive(Debug)]
440pub enum TracingError {
441 SpanNotFound(String),
442 InvalidTraceId(String),
443 Internal(String),
444 #[cfg(feature = "otlp")]
446 OtlpInitFailed(String),
447}
448
449impl std::fmt::Display for TracingError {
450 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451 match self {
452 TracingError::SpanNotFound(id) => write!(f, "Span not found: {}", id),
453 TracingError::InvalidTraceId(id) => write!(f, "Invalid trace id: {}", id),
454 TracingError::Internal(msg) => write!(f, "Tracing internal error: {}", msg),
455 #[cfg(feature = "otlp")]
456 TracingError::OtlpInitFailed(msg) => write!(f, "OTLP init failed: {}", msg),
457 }
458 }
459}
460
461impl std::error::Error for TracingError {}
462
463impl serde::Serialize for TracingError {
464 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
465 where
466 S: serde::Serializer,
467 {
468 serializer.serialize_str(&self.to_string())
469 }
470}
471
472#[derive(Debug, Clone)]
481pub struct LatencyHistogram {
482 samples: Vec<Duration>,
483 sum: Duration,
484}
485
486impl LatencyHistogram {
487 pub fn new(_buckets: Vec<Duration>) -> Self {
488 Self {
489 samples: Vec::new(),
490 sum: Duration::ZERO,
491 }
492 }
493
494 pub fn record(&mut self, duration: Duration) {
495 self.sum += duration;
496 let pos = self.samples.partition_point(|d| *d < duration);
497 self.samples.insert(pos, duration);
498 }
499
500 pub fn percentile(&self, p: f64) -> Option<Duration> {
501 if !(0.0..=100.0).contains(&p) || self.samples.is_empty() {
502 return None;
503 }
504 let n = self.samples.len();
505 let rank = ((p / 100.0) * n as f64).ceil() as usize;
509 let rank = rank.max(1).min(n);
510 Some(self.samples[rank - 1])
511 }
512
513 pub fn count(&self) -> usize {
514 self.samples.len()
515 }
516
517 pub fn mean(&self) -> Option<Duration> {
518 if self.samples.is_empty() {
519 None
520 } else {
521 Some(self.sum / self.samples.len() as u32)
522 }
523 }
524}
525
526#[derive(Debug)]
531pub struct ErrorRateCounter {
532 window: Duration,
533 samples: Vec<(std::time::Instant, bool)>,
534}
535
536impl ErrorRateCounter {
537 pub fn new(window: Duration) -> Self {
538 Self {
539 window,
540 samples: Vec::new(),
541 }
542 }
543
544 pub fn record(&mut self, success: bool) {
545 let now = std::time::Instant::now();
546 let cutoff = now - self.window;
547 self.samples.retain(|(ts, _)| *ts >= cutoff);
549 self.samples.push((now, success));
550 }
551
552 pub fn rate(&self) -> f64 {
553 if self.samples.is_empty() {
554 return 0.0;
555 }
556 let errors = self.samples.iter().filter(|(_, ok)| !ok).count() as f64;
557 errors / self.samples.len() as f64
558 }
559
560 pub fn total(&self) -> usize {
561 self.samples.len()
562 }
563
564 pub fn errors(&self) -> usize {
565 self.samples.iter().filter(|(_, ok)| !ok).count()
566 }
567}
568
569#[derive(Debug)]
580pub struct ErrorBudget {
581 slo_target: f64,
582 window: Duration,
583 samples: Vec<(std::time::Instant, usize)>,
584}
585
586impl ErrorBudget {
587 pub fn new(slo_target: f64, window: Duration) -> Self {
588 Self {
589 slo_target,
590 window,
591 samples: Vec::new(),
592 }
593 }
594
595 pub fn consume(&mut self, error_count: usize) {
596 let now = std::time::Instant::now();
597 let cutoff = now - self.window;
598 self.samples.retain(|(ts, _)| *ts >= cutoff);
599 if error_count > 0 {
600 self.samples.push((now, error_count));
601 }
602 }
603
604 fn total_errors_in_window(&self) -> usize {
605 let now = std::time::Instant::now();
606 let cutoff = now - self.window;
607 self.samples
608 .iter()
609 .filter(|(ts, _)| *ts >= cutoff)
610 .map(|(_, n)| *n)
611 .sum()
612 }
613
614 pub fn remaining(&self) -> f64 {
615 let total_errors = self.total_errors_in_window();
616 if total_errors == 0 {
617 return 1.0;
618 }
619 let error_budget = 1.0 - self.slo_target;
620 if error_budget <= 0.0 {
621 return 0.0;
623 }
624 let consumed = total_errors as f64 * error_budget;
625 (1.0 - consumed).clamp(0.0, 1.0)
626 }
627
628 pub fn is_exhausted(&self) -> bool {
629 self.remaining() == 0.0
630 }
631}
632
633#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
635pub enum AlertLevel {
636 Info,
637 Warning,
638 Critical,
639}
640
641#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
644pub struct Alert {
645 pub level: AlertLevel,
646 pub message: String,
647 pub timestamp: DateTime<Utc>,
648 pub operation: Option<String>,
649}
650
651#[derive(Debug)]
656pub struct SaturationGauge {
657 threshold: f64,
658 value: f64,
659}
660
661impl SaturationGauge {
662 pub fn new(threshold: f64) -> Self {
663 Self {
664 threshold,
665 value: 0.0,
666 }
667 }
668
669 pub fn set(&mut self, value: f64) {
670 self.value = value;
671 }
672
673 pub fn is_saturated(&self) -> bool {
674 self.value >= self.threshold
675 }
676
677 pub fn check_alert(&self) -> Option<Alert> {
678 if self.is_saturated() {
679 Some(Alert {
680 level: AlertLevel::Critical,
681 message: format!(
682 "saturation {:.2} exceeded threshold {:.2}",
683 self.value, self.threshold
684 ),
685 timestamp: Utc::now(),
686 operation: None,
687 })
688 } else {
689 None
690 }
691 }
692}
693
694pub trait AlertHook: Send + Sync {
700 fn notify(&self, alert: &Alert) -> Result<(), String>;
702}
703
704pub struct LogAlertHook;
709
710impl LogAlertHook {
711 pub fn new() -> Self {
712 Self
713 }
714}
715
716impl Default for LogAlertHook {
717 fn default() -> Self {
718 Self::new()
719 }
720}
721
722impl AlertHook for LogAlertHook {
723 fn notify(&self, alert: &Alert) -> Result<(), String> {
724 eprintln!(
725 "[SLA ALERT] level={:?} op={:?} ts={} msg={}",
726 alert.level, alert.operation, alert.timestamp, alert.message
727 );
728 Ok(())
729 }
730}
731
732pub struct InMemoryAlertHook {
745 identifier: String,
746 sent: RwLock<Vec<Alert>>,
747}
748
749impl InMemoryAlertHook {
750 pub fn new(identifier: String) -> Self {
752 Self {
753 identifier,
754 sent: RwLock::new(Vec::new()),
755 }
756 }
757
758 pub fn sent_alerts(&self) -> Vec<Alert> {
760 self.sent
761 .read()
762 .map(|guard| guard.clone())
763 .unwrap_or_default()
764 }
765
766 pub fn identifier(&self) -> &str {
768 &self.identifier
769 }
770
771 #[deprecated(
777 since = "1.2.0",
778 note = "use `identifier()` instead; this hook does not perform HTTP"
779 )]
780 pub fn url(&self) -> &str {
781 &self.identifier
782 }
783}
784
785impl AlertHook for InMemoryAlertHook {
786 fn notify(&self, alert: &Alert) -> Result<(), String> {
787 match self.sent.write() {
788 Ok(mut guard) => {
789 guard.push(alert.clone());
790 Ok(())
791 }
792 Err(e) => Err(format!("in-memory alert hook lock poisoned: {e}")),
793 }
794 }
795}
796
797struct OperationStats {
802 latency: LatencyHistogram,
803 total_count: usize,
804 error_count: usize,
805}
806
807impl OperationStats {
808 fn new() -> Self {
809 Self {
810 latency: LatencyHistogram::new(Vec::new()),
811 total_count: 0,
812 error_count: 0,
813 }
814 }
815
816 fn observe(&mut self, duration: Duration, success: bool) {
817 self.latency.record(duration);
818 self.total_count += 1;
819 if !success {
820 self.error_count += 1;
821 }
822 }
823
824 fn error_rate(&self) -> f64 {
825 if self.total_count == 0 {
826 0.0
827 } else {
828 self.error_count as f64 / self.total_count as f64
829 }
830 }
831}
832
833#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
835pub struct SlaReport {
836 pub p50_ms: f64,
838 pub p95_ms: f64,
840 pub p99_ms: f64,
842 pub error_rate: f64,
844 pub total_count: usize,
846 pub slo_target: f64,
848 pub error_budget_remaining: f64,
850 pub saturation: f64,
852}
853
854pub struct SlaMonitor {
859 slo_target: f64,
860 operations: RwLock<HashMap<String, OperationStats>>,
861}
862
863impl SlaMonitor {
864 pub fn new(slo_target: f64) -> Self {
865 Self {
866 slo_target,
867 operations: RwLock::new(HashMap::new()),
868 }
869 }
870
871 pub fn observe(&self, operation: &str, duration: Duration, success: bool) {
876 if let Ok(mut ops) = self.operations.write() {
877 let stats = ops
878 .entry(operation.to_string())
879 .or_insert_with(OperationStats::new);
880 stats.observe(duration, success);
881 }
882 }
883
884 pub fn report(&self, operation: &str) -> Option<SlaReport> {
885 let ops = self.operations.read().ok()?;
886 let stats = ops.get(operation)?;
887 let error_rate = stats.error_rate();
888 let error_budget = 1.0 - self.slo_target;
889 let (saturation, error_budget_remaining) = if error_budget <= 0.0 {
890 if error_rate == 0.0 {
892 (0.0, 1.0)
893 } else {
894 (1.0, 0.0)
895 }
896 } else {
897 let sat = (error_rate / error_budget).clamp(0.0, 1.0);
898 (sat, 1.0 - sat)
899 };
900 let ms = |p: f64| {
901 stats
902 .latency
903 .percentile(p)
904 .map(|d| d.as_nanos() as f64 / 1_000_000.0)
905 .unwrap_or(0.0)
906 };
907 Some(SlaReport {
908 p50_ms: ms(50.0),
909 p95_ms: ms(95.0),
910 p99_ms: ms(99.0),
911 error_rate,
912 total_count: stats.total_count,
913 slo_target: self.slo_target,
914 error_budget_remaining,
915 saturation,
916 })
917 }
918
919 pub fn operations(&self) -> Vec<String> {
920 self.operations
921 .read()
922 .map(|ops| ops.keys().cloned().collect())
923 .unwrap_or_default()
924 }
925}
926
927#[cfg(test)]
928mod tests {
929 use super::*;
930
931 #[test]
932 fn test_span_new() {
933 let span = Span::new("trace1", "span1", "operation1");
934 assert_eq!(span.trace_id, "trace1");
935 assert_eq!(span.span_id, "span1");
936 assert_eq!(span.operation_name, "operation1");
937 assert!(span.end_time.is_none());
938 }
939
940 #[test]
941 fn test_span_with_parent() {
942 let span = Span::new("trace1", "span1", "op").with_parent("parent1");
943 assert_eq!(span.parent_id, Some("parent1".to_string()));
944 }
945
946 #[test]
947 fn test_span_with_service() {
948 let span = Span::new("trace1", "span1", "op").with_service("my-service");
949 assert_eq!(span.service_name, "my-service");
950 }
951
952 #[test]
953 fn test_span_with_tag() {
954 let span = Span::new("trace1", "span1", "op").with_tag("key", "value");
955 assert_eq!(span.tags.get("key"), Some(&"value".to_string()));
956 }
957
958 #[test]
959 fn test_span_finish() {
960 let mut span = Span::new("trace1", "span1", "op");
961 span.finish();
962 assert!(span.end_time.is_some());
963 assert!(span.duration().is_some());
964 }
965
966 #[test]
967 fn test_span_add_log() {
968 let mut span = Span::new("trace1", "span1", "op");
969 span.add_log("test log");
970 assert_eq!(span.logs.len(), 1);
971 assert_eq!(span.logs[0].message, "test log");
972 }
973
974 #[test]
975 fn test_tracer_new() {
976 let tracer = SzTracer::new("test-service");
977 assert!(tracer.get_spans().is_empty());
978 }
979
980 #[test]
981 fn test_tracer_start_span() {
982 let tracer = SzTracer::new("test-service");
983 let span = tracer.start_span("test-operation");
984 assert_eq!(span.operation_name, "test-operation");
985 }
986
987 #[test]
988 fn test_tracer_end_span() {
989 let tracer = SzTracer::new("test-service");
990 let span = tracer.start_span("test-operation");
991 tracer.end_span(span);
992
993 let spans = tracer.get_spans();
994 assert_eq!(spans.len(), 1);
995 }
996
997 #[test]
998 fn test_tracer_inject() {
999 let tracer = SzTracer::new("test-service");
1000 let span = tracer.start_span("test");
1001 let headers = tracer.inject(&span);
1002
1003 let tp = headers
1005 .get("traceparent")
1006 .expect("traceparent header must be present");
1007 let parts: Vec<&str> = tp.split('-').collect();
1009 assert_eq!(parts.len(), 4);
1010 assert_eq!(parts[0], "00"); assert_eq!(parts[1], span.trace_id);
1012 assert_eq!(parts[2], span.span_id);
1013 assert_eq!(parts[3], "01"); }
1015
1016 #[test]
1017 fn test_tracer_extract() {
1018 let tracer = SzTracer::new("test-service");
1019 let mut headers = HashMap::new();
1020 let trace_id = "0af7651916cd43dd8448eb211c80319c";
1022 let span_id = "b7ad6b7169203331";
1023 headers.insert(
1024 "traceparent".to_string(),
1025 format!("00-{}-{}-01", trace_id, span_id),
1026 );
1027
1028 let span = tracer.extract(&headers);
1029 assert!(span.is_some());
1030 let span = span.unwrap();
1031 assert_eq!(span.trace_id, trace_id);
1032 assert_eq!(span.span_id, span_id);
1033 }
1034
1035 #[test]
1036 fn test_tracer_extract_legacy_headers() {
1037 let tracer = SzTracer::new("test-service");
1039 let mut headers = HashMap::new();
1040 headers.insert("trace-id".to_string(), "trace123".to_string());
1041 headers.insert("span-id".to_string(), "span456".to_string());
1042
1043 let span = tracer.extract(&headers);
1044 assert!(span.is_some());
1045 let span = span.unwrap();
1046 assert_eq!(span.trace_id, "trace123");
1047 assert_eq!(span.span_id, "span456");
1048 }
1049
1050 #[test]
1051 fn test_tracer_extract_missing_headers() {
1052 let tracer = SzTracer::new("test-service");
1053 let headers = HashMap::new();
1054 let span = tracer.extract(&headers);
1055 assert!(span.is_none());
1056 }
1057
1058 #[test]
1061 fn test_parse_traceparent_valid() {
1062 let valid = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
1063 let span = SzTracer::parse_traceparent(valid).expect("valid traceparent must parse");
1064 assert_eq!(span.trace_id, "0af7651916cd43dd8448eb211c80319c");
1065 assert_eq!(span.span_id, "b7ad6b7169203331");
1066 }
1067
1068 #[test]
1069 fn test_parse_traceparent_invalid_version() {
1070 assert!(SzTracer::parse_traceparent(
1072 "0-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
1073 )
1074 .is_none());
1075 assert!(SzTracer::parse_traceparent(
1077 "xy-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
1078 )
1079 .is_none());
1080 }
1081
1082 #[test]
1083 fn test_parse_traceparent_invalid_trace_id_length() {
1084 assert!(SzTracer::parse_traceparent("00-short-b7ad6b7169203331-01").is_none());
1086 }
1087
1088 #[test]
1089 fn test_parse_traceparent_invalid_span_id_length() {
1090 assert!(
1092 SzTracer::parse_traceparent("00-0af7651916cd43dd8448eb211c80319c-short-01").is_none()
1093 );
1094 }
1095
1096 #[test]
1097 fn test_parse_traceparent_all_zeros_trace_id_rejected() {
1098 let all_zero = "00-00000000000000000000000000000000-b7ad6b7169203331-01";
1100 assert!(SzTracer::parse_traceparent(all_zero).is_none());
1101 }
1102
1103 #[test]
1104 fn test_parse_traceparent_all_zeros_span_id_rejected() {
1105 let all_zero = "00-0af7651916cd43dd8448eb211c80319c-0000000000000000-01";
1107 assert!(SzTracer::parse_traceparent(all_zero).is_none());
1108 }
1109
1110 #[test]
1111 fn test_parse_traceparent_invalid_flags() {
1112 assert!(SzTracer::parse_traceparent(
1114 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-xyz"
1115 )
1116 .is_none());
1117 }
1118
1119 #[test]
1120 fn test_parse_traceparent_wrong_part_count() {
1121 assert!(SzTracer::parse_traceparent(
1123 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331"
1124 )
1125 .is_none());
1126 assert!(SzTracer::parse_traceparent(
1127 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01-extra"
1128 )
1129 .is_none());
1130 }
1131
1132 #[test]
1133 fn test_inject_legacy_preserves_old_format() {
1134 let tracer = SzTracer::new("svc");
1135 let span = tracer.start_span("op");
1136 let headers = tracer.inject_legacy(&span);
1137
1138 assert_eq!(headers.get("trace-id"), Some(&span.trace_id.to_string()));
1139 assert_eq!(headers.get("span-id"), Some(&span.span_id.to_string()));
1140 assert!(!headers.contains_key("parent-span-id"));
1141 }
1142
1143 #[test]
1144 fn test_extract_legacy_preserves_old_format() {
1145 let tracer = SzTracer::new("svc");
1146 let mut headers = HashMap::new();
1147 headers.insert("trace-id".to_string(), "abc".to_string());
1148 headers.insert("span-id".to_string(), "def".to_string());
1149
1150 let span = tracer.extract_legacy(&headers).expect("legacy extract");
1151 assert_eq!(span.trace_id, "abc");
1152 assert_eq!(span.span_id, "def");
1153 }
1154
1155 #[test]
1156 fn test_w3c_traceparent_roundtrip_preserves_ids() {
1157 let tracer = SzTracer::new("svc");
1159 let original = tracer.start_span("roundtrip");
1160 let headers = tracer.inject(&original);
1161
1162 let extracted = tracer.extract(&headers).expect("roundtrip extract");
1163 assert_eq!(extracted.trace_id(), original.trace_id());
1164 assert_eq!(extracted.span_id(), original.span_id());
1165 assert!(extracted.parent_id().is_none());
1166 }
1167
1168 #[test]
1169 fn test_w3c_prefers_traceparent_over_legacy() {
1170 let tracer = SzTracer::new("svc");
1172 let mut headers = HashMap::new();
1173 headers.insert(
1174 "traceparent".to_string(),
1175 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_string(),
1176 );
1177 headers.insert("trace-id".to_string(), "legacy-trace".to_string());
1178 headers.insert("span-id".to_string(), "legacy-span".to_string());
1179
1180 let span = tracer.extract(&headers).expect("extract");
1181 assert_eq!(span.trace_id, "0af7651916cd43dd8448eb211c80319c");
1182 assert_eq!(span.span_id, "b7ad6b7169203331");
1183 }
1184
1185 #[test]
1186 fn test_w3c_falls_back_to_legacy_on_invalid_traceparent() {
1187 let tracer = SzTracer::new("svc");
1189 let mut headers = HashMap::new();
1190 headers.insert("traceparent".to_string(), "invalid-format".to_string());
1191 headers.insert("trace-id".to_string(), "legacy-trace".to_string());
1192 headers.insert("span-id".to_string(), "legacy-span".to_string());
1193
1194 let span = tracer
1195 .extract(&headers)
1196 .expect("should fall back to legacy");
1197 assert_eq!(span.trace_id, "legacy-trace");
1198 assert_eq!(span.span_id, "legacy-span");
1199 }
1200
1201 #[test]
1202 fn test_otel_tracer() {
1203 let tracer = OtelTracer::new("test-service");
1204 let span = tracer.start_span("test-operation");
1205 assert_eq!(span.operation_name, "test-operation");
1206 }
1207
1208 #[test]
1209 fn test_otel_tracer_is_a_sz_tracer_wrapper_not_a_real_otel_sdk() {
1210 let tracer = OtelTracer::new("svc");
1214 assert!(tracer.inner().get_spans().is_empty());
1215
1216 let span = tracer.start_span("op");
1217 assert_eq!(span.service_name(), "svc");
1218 tracer.end_span(span);
1219
1220 let spans = tracer.inner().get_spans();
1221 assert_eq!(spans.len(), 1);
1222 assert_eq!(spans[0].operation_name(), "op");
1223 assert!(spans[0].end_time.is_some());
1224 }
1225
1226 #[test]
1227 fn test_otel_tracer_inject_extract_roundtrip_preserves_ids() {
1228 let tracer = OtelTracer::new("svc");
1230 let original = tracer.start_span("roundtrip");
1231 let headers = tracer.inject(&original);
1232
1233 let tp = headers
1235 .get("traceparent")
1236 .expect("traceparent must be present");
1237 assert!(tp.contains(original.trace_id()));
1238 assert!(tp.contains(original.span_id()));
1239 assert!(!headers.contains_key("parent-span-id"));
1240
1241 let extracted = tracer.extract(&headers).expect("extract should round-trip");
1242 assert_eq!(extracted.trace_id(), original.trace_id());
1243 assert_eq!(extracted.span_id(), original.span_id());
1244 assert!(extracted.parent_id().is_none());
1245 }
1246
1247 #[test]
1248 fn test_otel_tracer_preserves_parent_id_through_roundtrip() {
1249 let tracer = OtelTracer::new("svc");
1250 let parent = tracer.start_span("parent");
1251 let child = tracer
1252 .start_span("child")
1253 .with_parent(parent.span_id().to_string());
1254 let headers = tracer.inject(&child);
1255
1256 assert_eq!(
1258 headers.get("parent-span-id"),
1259 Some(&parent.span_id().to_string())
1260 );
1261
1262 let extracted = tracer.extract(&headers).expect("extract should round-trip");
1263 assert_eq!(extracted.parent_id(), Some(parent.span_id()));
1264 }
1265
1266 #[test]
1267 fn test_otel_tracer_extract_returns_none_without_required_headers() {
1268 let tracer = OtelTracer::new("svc");
1269 let headers: HashMap<String, String> = HashMap::new();
1270 assert!(tracer.extract(&headers).is_none());
1271
1272 let mut partial = HashMap::new();
1274 partial.insert("trace-id".to_string(), "abc".to_string());
1275 assert!(tracer.extract(&partial).is_none());
1277
1278 let mut bad_w3c = HashMap::new();
1280 bad_w3c.insert("traceparent".to_string(), "garbage".to_string());
1281 assert!(tracer.extract(&bad_w3c).is_none());
1282 }
1283
1284 #[test]
1285 fn test_otel_tracer_generated_ids_have_correct_length() {
1286 let trace_id = SzTracer::generate_trace_id();
1291 let span_id = SzTracer::generate_span_id();
1292 assert_eq!(trace_id.len(), 32, "trace_id must be 32 hex chars");
1293 assert_eq!(span_id.len(), 16, "span_id must be 16 hex chars");
1294
1295 assert!(trace_id.chars().all(|c| c.is_ascii_hexdigit()));
1297 assert!(span_id.chars().all(|c| c.is_ascii_hexdigit()));
1298 }
1299
1300 #[test]
1301 fn test_span_accessors() {
1302 let span = Span::new("trace1", "span1", "test-op")
1303 .with_service("svc")
1304 .with_tag("k", "v");
1305
1306 assert_eq!(span.trace_id(), "trace1");
1307 assert_eq!(span.span_id(), "span1");
1308 assert_eq!(span.operation_name(), "test-op");
1309 assert_eq!(span.service_name(), "svc");
1310 assert_eq!(span.tags().get("k"), Some(&"v".to_string()));
1311 }
1312
1313 #[test]
1314 fn test_generate_ids() {
1315 let trace_id = SzTracer::generate_trace_id();
1316 let span_id = SzTracer::generate_span_id();
1317
1318 assert_eq!(trace_id.len(), 32);
1319 assert_eq!(span_id.len(), 16);
1320 }
1321
1322 #[test]
1323 fn test_tracer_clear() {
1324 let tracer = SzTracer::new("test-service");
1325
1326 let span = tracer.start_span("op1");
1327 tracer.end_span(span);
1328 let span = tracer.start_span("op2");
1329 tracer.end_span(span);
1330
1331 assert_eq!(tracer.get_spans().len(), 2);
1332
1333 tracer.clear();
1334 assert!(tracer.get_spans().is_empty());
1335 }
1336
1337 #[test]
1340 fn test_latency_histogram_new_empty() {
1341 let hist = LatencyHistogram::new(vec![
1342 Duration::from_millis(10),
1343 Duration::from_millis(100),
1344 Duration::from_millis(1000),
1345 ]);
1346 assert_eq!(hist.count(), 0);
1347 assert!(hist.percentile(50.0).is_none());
1348 assert!(hist.mean().is_none());
1349 }
1350
1351 #[test]
1352 fn test_latency_histogram_record_single() {
1353 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1354 hist.record(Duration::from_millis(50));
1355 assert_eq!(hist.count(), 1);
1356 assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(50)));
1357 assert_eq!(hist.mean(), Some(Duration::from_millis(50)));
1358 }
1359
1360 #[test]
1361 fn test_latency_histogram_percentile_p50_sorted() {
1362 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1363 for ms in [10, 20, 30, 40, 50] {
1364 hist.record(Duration::from_millis(ms));
1365 }
1366 assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(30)));
1368 }
1369
1370 #[test]
1371 fn test_latency_histogram_percentile_p95_high_value() {
1372 let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1373 for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1374 hist.record(Duration::from_millis(ms));
1375 }
1376 let p95 = hist.percentile(95.0).expect("p95 must exist");
1378 assert!(p95 >= Duration::from_millis(9));
1379 }
1380
1381 #[test]
1382 fn test_latency_histogram_percentile_p99_max_value() {
1383 let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1384 for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1385 hist.record(Duration::from_millis(ms));
1386 }
1387 let p99 = hist.percentile(99.0).expect("p99 must exist");
1388 assert_eq!(p99, Duration::from_millis(100));
1389 }
1390
1391 #[test]
1392 fn test_latency_histogram_percentile_p0_min_value() {
1393 let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1394 for ms in [10, 20, 30] {
1395 hist.record(Duration::from_millis(ms));
1396 }
1397 assert_eq!(hist.percentile(0.0), Some(Duration::from_millis(10)));
1398 }
1399
1400 #[test]
1401 fn test_latency_histogram_percentile_p100_max_value() {
1402 let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1403 for ms in [10, 20, 30] {
1404 hist.record(Duration::from_millis(ms));
1405 }
1406 assert_eq!(hist.percentile(100.0), Some(Duration::from_millis(30)));
1407 }
1408
1409 #[test]
1410 fn test_latency_histogram_percentile_empty_returns_none() {
1411 let hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1412 assert!(hist.percentile(50.0).is_none());
1413 }
1414
1415 #[test]
1416 fn test_latency_histogram_percentile_out_of_range_returns_none() {
1417 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1418 hist.record(Duration::from_millis(50));
1419 assert!(hist.percentile(-1.0).is_none());
1420 assert!(hist.percentile(101.0).is_none());
1421 }
1422
1423 #[test]
1424 fn test_latency_histogram_count() {
1425 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1426 assert_eq!(hist.count(), 0);
1427 hist.record(Duration::from_millis(10));
1428 hist.record(Duration::from_millis(20));
1429 hist.record(Duration::from_millis(30));
1430 assert_eq!(hist.count(), 3);
1431 }
1432
1433 #[test]
1434 fn test_latency_histogram_mean_multiple() {
1435 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1436 for ms in [10, 20, 30, 40, 50] {
1437 hist.record(Duration::from_millis(ms));
1438 }
1439 assert_eq!(hist.mean(), Some(Duration::from_millis(30)));
1441 }
1442
1443 #[test]
1444 fn test_latency_histogram_record_unsorted_input_stays_sorted() {
1445 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1446 hist.record(Duration::from_millis(50));
1447 hist.record(Duration::from_millis(10));
1448 hist.record(Duration::from_millis(30));
1449 assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(30)));
1451 }
1452
1453 #[test]
1456 fn test_error_rate_counter_new_empty() {
1457 let counter = ErrorRateCounter::new(Duration::from_secs(60));
1458 assert_eq!(counter.total(), 0);
1459 assert_eq!(counter.errors(), 0);
1460 assert_eq!(counter.rate(), 0.0);
1461 }
1462
1463 #[test]
1464 fn test_error_rate_counter_all_success_rate_zero() {
1465 let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1466 for _ in 0..10 {
1467 counter.record(true);
1468 }
1469 assert_eq!(counter.total(), 10);
1470 assert_eq!(counter.errors(), 0);
1471 assert_eq!(counter.rate(), 0.0);
1472 }
1473
1474 #[test]
1475 fn test_error_rate_counter_all_failures_rate_one() {
1476 let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1477 for _ in 0..10 {
1478 counter.record(false);
1479 }
1480 assert_eq!(counter.total(), 10);
1481 assert_eq!(counter.errors(), 10);
1482 assert_eq!(counter.rate(), 1.0);
1483 }
1484
1485 #[test]
1486 fn test_error_rate_counter_mixed_rate() {
1487 let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1488 for _ in 0..7 {
1490 counter.record(true);
1491 }
1492 for _ in 0..3 {
1493 counter.record(false);
1494 }
1495 assert_eq!(counter.total(), 10);
1496 assert_eq!(counter.errors(), 3);
1497 let rate = counter.rate();
1498 assert!((rate - 0.3).abs() < 1e-9, "expected 0.3, got {rate}");
1499 }
1500
1501 #[test]
1502 fn test_error_rate_counter_empty_rate_is_zero() {
1503 let counter = ErrorRateCounter::new(Duration::from_secs(60));
1504 assert_eq!(counter.rate(), 0.0);
1506 }
1507
1508 #[test]
1509 fn test_error_rate_counter_window_expires_old_samples() {
1510 let mut counter = ErrorRateCounter::new(Duration::from_millis(100));
1512 counter.record(false);
1513 counter.record(false);
1514 std::thread::sleep(Duration::from_millis(120));
1516 counter.record(true);
1517 assert_eq!(counter.total(), 1);
1519 assert_eq!(counter.errors(), 0);
1520 assert_eq!(counter.rate(), 0.0);
1521 }
1522
1523 #[test]
1526 fn test_error_budget_new_is_full() {
1527 let budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1528 assert!((budget.remaining() - 1.0).abs() < 1e-9);
1529 assert!(!budget.is_exhausted());
1530 }
1531
1532 #[test]
1533 fn test_error_budget_consume_reduces_remaining() {
1534 let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1535 budget.consume(1);
1537 assert!((budget.remaining() - 0.999).abs() < 1e-9);
1539 assert!(!budget.is_exhausted());
1540 }
1541
1542 #[test]
1543 fn test_error_budget_exhausted_at_capacity() {
1544 let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1545 budget.consume(1000);
1547 assert_eq!(budget.remaining(), 0.0);
1548 assert!(budget.is_exhausted());
1549 }
1550
1551 #[test]
1552 fn test_error_budget_over_consume_clamps_to_zero() {
1553 let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1554 budget.consume(2000);
1555 assert_eq!(budget.remaining(), 0.0);
1556 assert!(budget.is_exhausted());
1557 }
1558
1559 #[test]
1560 fn test_error_budget_zero_errors_returns_one() {
1561 let budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1562 assert_eq!(budget.remaining(), 1.0);
1563 assert!(!budget.is_exhausted());
1564 }
1565
1566 #[test]
1567 fn test_error_budget_window_refills_after_expiry() {
1568 let mut budget = ErrorBudget::new(0.5, Duration::from_millis(100));
1570 budget.consume(2);
1571 assert!(budget.is_exhausted());
1572 std::thread::sleep(Duration::from_millis(120));
1574 assert!((budget.remaining() - 1.0).abs() < 1e-9);
1575 assert!(!budget.is_exhausted());
1576 }
1577
1578 #[test]
1579 fn test_error_budget_slo_one_means_no_errors_allowed() {
1580 let mut budget = ErrorBudget::new(1.0, Duration::from_secs(60));
1582 assert_eq!(budget.remaining(), 1.0);
1583 budget.consume(1);
1584 assert_eq!(budget.remaining(), 0.0);
1585 assert!(budget.is_exhausted());
1586 }
1587
1588 #[test]
1589 fn test_error_budget_multiple_consumes_accumulate() {
1590 let mut budget = ErrorBudget::new(0.99, Duration::from_secs(60));
1591 budget.consume(30);
1593 assert!((budget.remaining() - 0.7).abs() < 1e-9);
1594 budget.consume(30);
1595 assert!((budget.remaining() - 0.4).abs() < 1e-9);
1596 budget.consume(40);
1597 assert_eq!(budget.remaining(), 0.0);
1598 assert!(budget.is_exhausted());
1599 }
1600
1601 #[test]
1604 fn test_alert_level_variants_exist() {
1605 let info = AlertLevel::Info;
1606 let warning = AlertLevel::Warning;
1607 let critical = AlertLevel::Critical;
1608 assert_ne!(format!("{info:?}"), format!("{warning:?}"));
1610 assert_ne!(format!("{warning:?}"), format!("{critical:?}"));
1611 assert_ne!(format!("{info:?}"), format!("{critical:?}"));
1612 }
1613
1614 #[test]
1615 fn test_alert_construction_with_all_fields() {
1616 let ts = Utc::now();
1617 let alert = Alert {
1618 level: AlertLevel::Critical,
1619 message: "p99 latency exceeded budget".to_string(),
1620 timestamp: ts,
1621 operation: Some("query_user".to_string()),
1622 };
1623 assert_eq!(alert.level, AlertLevel::Critical);
1624 assert_eq!(alert.message, "p99 latency exceeded budget");
1625 assert_eq!(alert.timestamp, ts);
1626 assert_eq!(alert.operation.as_deref(), Some("query_user"));
1627 }
1628
1629 #[test]
1630 fn test_alert_construction_without_operation() {
1631 let alert = Alert {
1632 level: AlertLevel::Info,
1633 message: "system healthy".to_string(),
1634 timestamp: Utc::now(),
1635 operation: None,
1636 };
1637 assert!(alert.operation.is_none());
1638 }
1639
1640 #[test]
1641 fn test_alert_implements_clone_debug() {
1642 let alert = Alert {
1643 level: AlertLevel::Warning,
1644 message: "approaching budget".to_string(),
1645 timestamp: Utc::now(),
1646 operation: Some("op".to_string()),
1647 };
1648 let cloned = alert.clone();
1649 assert_eq!(cloned.level, alert.level);
1650 assert_eq!(cloned.message, alert.message);
1651 let debug_str = format!("{alert:?}");
1653 assert!(
1654 debug_str.contains("approaching budget"),
1655 "debug output missing message: {}",
1656 debug_str
1657 );
1658 assert!(
1659 debug_str.contains("Warning"),
1660 "debug output missing level: {}",
1661 debug_str
1662 );
1663 }
1664
1665 #[test]
1668 fn test_saturation_gauge_new_starts_unsaturated() {
1669 let gauge = SaturationGauge::new(0.8);
1670 assert!(!gauge.is_saturated());
1671 assert!(gauge.check_alert().is_none());
1672 }
1673
1674 #[test]
1675 fn test_saturation_gauge_set_below_threshold_not_saturated() {
1676 let mut gauge = SaturationGauge::new(0.8);
1677 gauge.set(0.5);
1678 assert!(!gauge.is_saturated());
1679 assert!(gauge.check_alert().is_none());
1680 }
1681
1682 #[test]
1683 fn test_saturation_gauge_set_at_threshold_is_saturated() {
1684 let mut gauge = SaturationGauge::new(0.8);
1685 gauge.set(0.8);
1686 assert!(gauge.is_saturated());
1687 }
1688
1689 #[test]
1690 fn test_saturation_gauge_set_above_threshold_is_saturated() {
1691 let mut gauge = SaturationGauge::new(0.8);
1692 gauge.set(0.95);
1693 assert!(gauge.is_saturated());
1694 }
1695
1696 #[test]
1697 fn test_saturation_gauge_check_alert_returns_critical_when_saturated() {
1698 let mut gauge = SaturationGauge::new(0.8);
1699 gauge.set(0.95);
1700 let alert = gauge.check_alert().expect("alert must fire when saturated");
1701 assert_eq!(alert.level, AlertLevel::Critical);
1702 assert!(!alert.message.is_empty());
1703 assert!(alert.operation.is_none()); }
1705
1706 #[test]
1707 fn test_saturation_gauge_check_alert_returns_none_when_not_saturated() {
1708 let mut gauge = SaturationGauge::new(0.8);
1709 gauge.set(0.3);
1710 assert!(gauge.check_alert().is_none());
1711 }
1712
1713 #[test]
1714 fn test_saturation_gauge_set_zero_not_saturated() {
1715 let mut gauge = SaturationGauge::new(0.8);
1716 gauge.set(0.0);
1717 assert!(!gauge.is_saturated());
1718 }
1719
1720 #[test]
1721 fn test_saturation_gauge_set_one_saturated() {
1722 let mut gauge = SaturationGauge::new(0.5);
1723 gauge.set(1.0);
1724 assert!(gauge.is_saturated());
1725 }
1726
1727 #[test]
1728 fn test_saturation_gauge_threshold_zero_always_saturated() {
1729 let mut gauge = SaturationGauge::new(0.0);
1730 gauge.set(0.0);
1731 assert!(gauge.is_saturated());
1734 }
1735
1736 fn sample_alert(level: AlertLevel, op: Option<&str>) -> Alert {
1739 Alert {
1740 level,
1741 message: "test alert".to_string(),
1742 timestamp: Utc::now(),
1743 operation: op.map(str::to_string),
1744 }
1745 }
1746
1747 #[test]
1748 fn test_log_alert_hook_notify_returns_ok() {
1749 let hook = LogAlertHook::new();
1750 let alert = sample_alert(AlertLevel::Warning, Some("op"));
1751 let result = hook.notify(&alert);
1752 assert!(result.is_ok());
1753 }
1754
1755 #[test]
1756 fn test_log_alert_hook_notify_critical_succeeds() {
1757 let hook = LogAlertHook::new();
1758 let alert = sample_alert(AlertLevel::Critical, None);
1759 let result = hook.notify(&alert);
1760 assert!(result.is_ok());
1761 }
1762
1763 #[test]
1764 fn test_log_alert_hook_implements_send_sync() {
1765 fn assert_send_sync<T: Send + Sync>() {}
1766 assert_send_sync::<LogAlertHook>();
1767 }
1768
1769 #[test]
1770 fn test_webhook_alert_hook_new_starts_empty() {
1771 let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1772 assert!(hook.sent_alerts().is_empty());
1773 }
1774
1775 #[test]
1776 fn test_webhook_alert_hook_notify_stores_alert() {
1777 let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1778 let alert = sample_alert(AlertLevel::Critical, Some("query_user"));
1779 hook.notify(&alert).expect("notify must succeed");
1780 let sent = hook.sent_alerts();
1781 assert_eq!(sent.len(), 1);
1782 assert_eq!(sent[0], alert);
1783 }
1784
1785 #[test]
1786 fn test_webhook_alert_hook_multiple_notifications_accumulate() {
1787 let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1788 let a1 = sample_alert(AlertLevel::Info, None);
1789 let a2 = sample_alert(AlertLevel::Warning, Some("op1"));
1790 let a3 = sample_alert(AlertLevel::Critical, Some("op2"));
1791 hook.notify(&a1).unwrap();
1792 hook.notify(&a2).unwrap();
1793 hook.notify(&a3).unwrap();
1794 let sent = hook.sent_alerts();
1795 assert_eq!(sent.len(), 3);
1796 assert_eq!(sent[0], a1);
1797 assert_eq!(sent[1], a2);
1798 assert_eq!(sent[2], a3);
1799 }
1800
1801 #[test]
1802 fn test_webhook_alert_hook_sent_alerts_returns_clone() {
1803 let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1805 let alert = sample_alert(AlertLevel::Info, None);
1806 hook.notify(&alert).unwrap();
1807 let mut sent = hook.sent_alerts();
1808 sent.clear();
1809 assert_eq!(hook.sent_alerts().len(), 1);
1811 }
1812
1813 #[test]
1814 fn test_webhook_alert_hook_implements_send_sync() {
1815 fn assert_send_sync<T: Send + Sync>() {}
1816 assert_send_sync::<InMemoryAlertHook>();
1817 }
1818
1819 #[test]
1820 fn test_alert_hook_trait_object_dispatch() {
1821 let hooks: Vec<Box<dyn AlertHook>> = vec![
1823 Box::new(LogAlertHook::new()),
1824 Box::new(InMemoryAlertHook::new(
1825 "https://example.com/hook".to_string(),
1826 )),
1827 ];
1828 let alert = sample_alert(AlertLevel::Critical, Some("op"));
1829 for hook in &hooks {
1830 assert!(hook.notify(&alert).is_ok());
1831 }
1832 let webhook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1835 webhook.notify(&alert).unwrap();
1836 assert_eq!(webhook.sent_alerts().len(), 1);
1837 }
1838
1839 #[test]
1842 fn test_sla_monitor_new_empty() {
1843 let monitor = SlaMonitor::new(0.999);
1844 assert!(monitor.operations().is_empty());
1845 }
1846
1847 #[test]
1848 fn test_sla_monitor_observe_creates_operation() {
1849 let monitor = SlaMonitor::new(0.999);
1850 monitor.observe("query", Duration::from_millis(50), true);
1851 let ops = monitor.operations();
1852 assert_eq!(ops, vec!["query".to_string()]);
1853 }
1854
1855 #[test]
1856 fn test_sla_monitor_report_unknown_returns_none() {
1857 let monitor = SlaMonitor::new(0.999);
1858 assert!(monitor.report("unknown").is_none());
1859 }
1860
1861 #[test]
1862 fn test_sla_monitor_report_basic_stats_all_success() {
1863 let monitor = SlaMonitor::new(0.999);
1864 for ms in [10, 20, 30, 40, 50] {
1865 monitor.observe("op", Duration::from_millis(ms), true);
1866 }
1867 let report = monitor.report("op").expect("report must exist");
1868 assert_eq!(report.total_count, 5);
1869 assert_eq!(report.error_rate, 0.0);
1870 assert!((report.slo_target - 0.999).abs() < 1e-9);
1871 assert!((report.p50_ms - 30.0).abs() < 1e-9);
1873 assert!((report.error_budget_remaining - 1.0).abs() < 1e-9);
1875 assert!((report.saturation - 0.0).abs() < 1e-9);
1876 }
1877
1878 #[test]
1879 fn test_sla_monitor_report_with_errors_over_budget() {
1880 let monitor = SlaMonitor::new(0.999);
1881 for _ in 0..8 {
1883 monitor.observe("op", Duration::from_millis(10), true);
1884 }
1885 for _ in 0..2 {
1886 monitor.observe("op", Duration::from_millis(10), false);
1887 }
1888 let report = monitor.report("op").expect("report must exist");
1889 assert_eq!(report.total_count, 10);
1890 assert!((report.error_rate - 0.2).abs() < 1e-9);
1891 assert!((report.saturation - 1.0).abs() < 1e-9);
1894 assert!((report.error_budget_remaining - 0.0).abs() < 1e-9);
1895 }
1896
1897 #[test]
1898 fn test_sla_monitor_report_partial_budget() {
1899 let monitor = SlaMonitor::new(0.9);
1904 for i in 0..20 {
1905 monitor.observe("op", Duration::from_millis(i), i != 5);
1906 }
1907 let report = monitor.report("op").expect("report");
1908 assert!((report.error_rate - 0.05).abs() < 1e-9);
1909 assert!((report.saturation - 0.5).abs() < 1e-9);
1910 assert!((report.error_budget_remaining - 0.5).abs() < 1e-9);
1911 }
1912
1913 #[test]
1914 fn test_sla_monitor_operations_isolated() {
1915 let monitor = SlaMonitor::new(0.999);
1916 monitor.observe("op1", Duration::from_millis(10), true);
1917 monitor.observe("op2", Duration::from_millis(20), false);
1918 let mut ops = monitor.operations();
1919 ops.sort();
1920 assert_eq!(ops, vec!["op1".to_string(), "op2".to_string()]);
1921
1922 let r1 = monitor.report("op1").expect("op1 report");
1923 let r2 = monitor.report("op2").expect("op2 report");
1924 assert_eq!(r1.total_count, 1);
1925 assert_eq!(r2.total_count, 1);
1926 assert!((r1.error_rate - 0.0).abs() < 1e-9);
1927 assert!((r2.error_rate - 1.0).abs() < 1e-9);
1928 }
1929
1930 #[test]
1931 fn test_sla_monitor_p95_p99_high_percentile() {
1932 let monitor = SlaMonitor::new(0.999);
1933 for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1934 monitor.observe("op", Duration::from_millis(ms), true);
1935 }
1936 let report = monitor.report("op").expect("report");
1937 assert!((report.p95_ms - 100.0).abs() < 1e-9);
1939 assert!((report.p99_ms - 100.0).abs() < 1e-9);
1940 }
1941
1942 #[test]
1943 fn test_sla_monitor_observe_aggregates_multiple_calls() {
1944 let monitor = SlaMonitor::new(0.999);
1945 for _ in 0..100 {
1946 monitor.observe("op", Duration::from_millis(5), true);
1947 }
1948 let report = monitor.report("op").expect("report");
1949 assert_eq!(report.total_count, 100);
1950 assert!((report.p50_ms - 5.0).abs() < 1e-9);
1951 }
1952
1953 #[test]
1954 fn test_sla_monitor_implements_send_sync() {
1955 fn assert_send_sync<T: Send + Sync>() {}
1956 assert_send_sync::<SlaMonitor>();
1957 }
1958
1959 #[test]
1960 fn test_sla_monitor_concurrent_observe_thread_safe() {
1961 use std::sync::Arc;
1962 use std::thread;
1963
1964 let monitor = Arc::new(SlaMonitor::new(0.999));
1965 let mut handles = vec![];
1966
1967 for t in 0..4 {
1968 let m = Arc::clone(&monitor);
1969 handles.push(thread::spawn(move || {
1970 for i in 0..100 {
1971 m.observe("op", Duration::from_millis(i as u64), i % 10 != 0);
1973 }
1974 let op_name = format!("thread-{t}");
1976 m.observe(&op_name, Duration::from_millis(1), true);
1977 }));
1978 }
1979
1980 for h in handles {
1981 h.join().unwrap();
1982 }
1983
1984 let report = monitor.report("op").expect("op report");
1985 assert_eq!(report.total_count, 400);
1986 assert!((report.error_rate - 0.1).abs() < 1e-9);
1988
1989 let mut ops = monitor.operations();
1991 ops.sort();
1992 assert_eq!(ops.len(), 5);
1994 assert!(ops.contains(&"op".to_string()));
1995 }
1996
1997 #[test]
1998 fn test_sla_monitor_report_slo_one_with_no_errors() {
1999 let monitor = SlaMonitor::new(1.0);
2001 monitor.observe("op", Duration::from_millis(10), true);
2002 let report = monitor.report("op").expect("report");
2003 assert!((report.error_budget_remaining - 1.0).abs() < 1e-9);
2004 assert!((report.saturation - 0.0).abs() < 1e-9);
2005 }
2006
2007 #[test]
2008 fn test_sla_monitor_report_slo_one_with_errors() {
2009 let monitor = SlaMonitor::new(1.0);
2011 monitor.observe("op", Duration::from_millis(10), false);
2012 let report = monitor.report("op").expect("report");
2013 assert!((report.error_budget_remaining - 0.0).abs() < 1e-9);
2014 assert!((report.saturation - 1.0).abs() < 1e-9);
2015 }
2016
2017 #[test]
2018 fn test_sla_report_fields_are_public() {
2019 let monitor = SlaMonitor::new(0.999);
2021 monitor.observe("op", Duration::from_millis(10), true);
2022 let r = monitor.report("op").unwrap();
2023 let _p50: f64 = r.p50_ms;
2024 let _p95: f64 = r.p95_ms;
2025 let _p99: f64 = r.p99_ms;
2026 let _erate: f64 = r.error_rate;
2027 let _total: usize = r.total_count;
2028 let _slo: f64 = r.slo_target;
2029 let _ebr: f64 = r.error_budget_remaining;
2030 let _sat: f64 = r.saturation;
2031 }
2032}
2033
2034#[cfg(feature = "otlp")]
2056#[derive(Debug, Clone)]
2057pub struct OtlpConfig {
2058 pub endpoint: String,
2060 pub service_name: String,
2062 pub timeout_ms: u64,
2064}
2065
2066#[cfg(feature = "otlp")]
2067impl Default for OtlpConfig {
2068 fn default() -> Self {
2069 Self {
2070 endpoint: "http://localhost:4317".to_string(),
2071 service_name: "sz-orm".to_string(),
2072 timeout_ms: 5000,
2073 }
2074 }
2075}
2076
2077#[cfg(feature = "otlp")]
2099pub async fn init_otlp_exporter(config: OtlpConfig) -> Result<OtlpGuard, TracingError> {
2100 use opentelemetry_otlp::{SpanExporter, WithExportConfig};
2101 use opentelemetry_sdk::resource::Resource;
2102 use opentelemetry_sdk::runtime::Tokio;
2103 use opentelemetry_sdk::trace::TracerProvider;
2104 use std::time::Duration;
2105
2106 let exporter = SpanExporter::builder()
2107 .with_tonic()
2108 .with_endpoint(config.endpoint.clone())
2109 .with_timeout(Duration::from_millis(config.timeout_ms))
2110 .build()
2111 .map_err(|e| TracingError::OtlpInitFailed(format!("exporter build: {e}")))?;
2112
2113 let provider = TracerProvider::builder()
2114 .with_batch_exporter(exporter, Tokio)
2115 .with_resource(Resource::new_with_defaults([opentelemetry::KeyValue::new(
2116 "service.name",
2117 config.service_name.clone(),
2118 )]))
2119 .build();
2120
2121 opentelemetry::global::set_tracer_provider(provider.clone());
2123
2124 Ok(OtlpGuard { provider })
2125}
2126
2127#[cfg(feature = "otlp")]
2131pub struct OtlpGuard {
2132 provider: opentelemetry_sdk::trace::TracerProvider,
2133}
2134
2135#[cfg(feature = "otlp")]
2136impl Drop for OtlpGuard {
2137 fn drop(&mut self) {
2138 let _ = self.provider.shutdown();
2140 }
2141}