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 {
388 tracer: SzTracer,
389}
390
391impl OtelTracer {
392 pub fn new(service_name: impl Into<String>) -> Self {
394 Self {
395 tracer: SzTracer::new(service_name),
396 }
397 }
398
399 pub fn inner(&self) -> &SzTracer {
402 &self.tracer
403 }
404}
405
406impl Tracer for OtelTracer {
407 fn start_span(&self, operation_name: &str) -> Span {
408 self.tracer.start_span(operation_name)
409 }
410
411 fn end_span(&self, span: Span) {
412 self.tracer.end_span(span)
413 }
414
415 fn inject(&self, span: &Span) -> HashMap<String, String> {
416 self.tracer.inject(span)
417 }
418
419 fn extract(&self, headers: &HashMap<String, String>) -> Option<Span> {
420 self.tracer.extract(headers)
421 }
422}
423
424fn current_timestamp() -> i64 {
425 use std::time::{SystemTime, UNIX_EPOCH};
426 SystemTime::now()
427 .duration_since(UNIX_EPOCH)
428 .unwrap_or_default()
429 .as_millis() as i64
430}
431
432fn rand_u64() -> u64 {
433 use std::collections::hash_map::RandomState;
434 use std::hash::{BuildHasher, Hasher};
435 RandomState::new().build_hasher().finish()
436}
437
438#[derive(Debug)]
439pub enum TracingError {
440 SpanNotFound(String),
441 InvalidTraceId(String),
442 Internal(String),
443 #[cfg(feature = "otlp")]
445 OtlpInitFailed(String),
446}
447
448impl std::fmt::Display for TracingError {
449 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450 match self {
451 TracingError::SpanNotFound(id) => write!(f, "Span not found: {}", id),
452 TracingError::InvalidTraceId(id) => write!(f, "Invalid trace id: {}", id),
453 TracingError::Internal(msg) => write!(f, "Tracing internal error: {}", msg),
454 #[cfg(feature = "otlp")]
455 TracingError::OtlpInitFailed(msg) => write!(f, "OTLP init failed: {}", msg),
456 }
457 }
458}
459
460impl std::error::Error for TracingError {}
461
462impl serde::Serialize for TracingError {
463 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
464 where
465 S: serde::Serializer,
466 {
467 serializer.serialize_str(&self.to_string())
468 }
469}
470
471#[derive(Debug, Clone)]
479pub struct LatencyHistogram {
480 samples: Vec<Duration>,
481 sum: Duration,
482}
483
484impl LatencyHistogram {
485 pub fn new(_buckets: Vec<Duration>) -> Self {
486 Self {
487 samples: Vec::new(),
488 sum: Duration::ZERO,
489 }
490 }
491
492 pub fn record(&mut self, duration: Duration) {
493 self.sum += duration;
494 let pos = self.samples.partition_point(|d| *d < duration);
495 self.samples.insert(pos, duration);
496 }
497
498 pub fn percentile(&self, p: f64) -> Option<Duration> {
499 if !(0.0..=100.0).contains(&p) || self.samples.is_empty() {
500 return None;
501 }
502 let n = self.samples.len();
503 let rank = ((p / 100.0) * n as f64).ceil() as usize;
507 let rank = rank.max(1).min(n);
508 Some(self.samples[rank - 1])
509 }
510
511 pub fn count(&self) -> usize {
512 self.samples.len()
513 }
514
515 pub fn mean(&self) -> Option<Duration> {
516 if self.samples.is_empty() {
517 None
518 } else {
519 Some(self.sum / self.samples.len() as u32)
520 }
521 }
522}
523
524#[derive(Debug)]
529pub struct ErrorRateCounter {
530 window: Duration,
531 samples: Vec<(std::time::Instant, bool)>,
532}
533
534impl ErrorRateCounter {
535 pub fn new(window: Duration) -> Self {
536 Self {
537 window,
538 samples: Vec::new(),
539 }
540 }
541
542 pub fn record(&mut self, success: bool) {
543 let now = std::time::Instant::now();
544 let cutoff = now - self.window;
545 self.samples.retain(|(ts, _)| *ts >= cutoff);
547 self.samples.push((now, success));
548 }
549
550 pub fn rate(&self) -> f64 {
551 if self.samples.is_empty() {
552 return 0.0;
553 }
554 let errors = self.samples.iter().filter(|(_, ok)| !ok).count() as f64;
555 errors / self.samples.len() as f64
556 }
557
558 pub fn total(&self) -> usize {
559 self.samples.len()
560 }
561
562 pub fn errors(&self) -> usize {
563 self.samples.iter().filter(|(_, ok)| !ok).count()
564 }
565}
566
567#[derive(Debug)]
576pub struct ErrorBudget {
577 slo_target: f64,
578 window: Duration,
579 samples: Vec<(std::time::Instant, usize)>,
580}
581
582impl ErrorBudget {
583 pub fn new(slo_target: f64, window: Duration) -> Self {
584 Self {
585 slo_target,
586 window,
587 samples: Vec::new(),
588 }
589 }
590
591 pub fn consume(&mut self, error_count: usize) {
592 let now = std::time::Instant::now();
593 let cutoff = now - self.window;
594 self.samples.retain(|(ts, _)| *ts >= cutoff);
595 if error_count > 0 {
596 self.samples.push((now, error_count));
597 }
598 }
599
600 fn total_errors_in_window(&self) -> usize {
601 let now = std::time::Instant::now();
602 let cutoff = now - self.window;
603 self.samples
604 .iter()
605 .filter(|(ts, _)| *ts >= cutoff)
606 .map(|(_, n)| *n)
607 .sum()
608 }
609
610 pub fn remaining(&self) -> f64 {
611 let total_errors = self.total_errors_in_window();
612 if total_errors == 0 {
613 return 1.0;
614 }
615 let error_budget = 1.0 - self.slo_target;
616 if error_budget <= 0.0 {
617 return 0.0;
619 }
620 let consumed = total_errors as f64 * error_budget;
621 (1.0 - consumed).clamp(0.0, 1.0)
622 }
623
624 pub fn is_exhausted(&self) -> bool {
625 self.remaining() == 0.0
626 }
627}
628
629#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
631pub enum AlertLevel {
632 Info,
633 Warning,
634 Critical,
635}
636
637#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
640pub struct Alert {
641 pub level: AlertLevel,
642 pub message: String,
643 pub timestamp: DateTime<Utc>,
644 pub operation: Option<String>,
645}
646
647#[derive(Debug)]
652pub struct SaturationGauge {
653 threshold: f64,
654 value: f64,
655}
656
657impl SaturationGauge {
658 pub fn new(threshold: f64) -> Self {
659 Self {
660 threshold,
661 value: 0.0,
662 }
663 }
664
665 pub fn set(&mut self, value: f64) {
666 self.value = value;
667 }
668
669 pub fn is_saturated(&self) -> bool {
670 self.value >= self.threshold
671 }
672
673 pub fn check_alert(&self) -> Option<Alert> {
674 if self.is_saturated() {
675 Some(Alert {
676 level: AlertLevel::Critical,
677 message: format!(
678 "saturation {:.2} exceeded threshold {:.2}",
679 self.value, self.threshold
680 ),
681 timestamp: Utc::now(),
682 operation: None,
683 })
684 } else {
685 None
686 }
687 }
688}
689
690pub trait AlertHook: Send + Sync {
694 fn notify(&self, alert: &Alert) -> Result<(), String>;
696}
697
698pub struct LogAlertHook;
702
703impl LogAlertHook {
704 pub fn new() -> Self {
705 Self
706 }
707}
708
709impl Default for LogAlertHook {
710 fn default() -> Self {
711 Self::new()
712 }
713}
714
715impl AlertHook for LogAlertHook {
716 fn notify(&self, alert: &Alert) -> Result<(), String> {
717 eprintln!(
718 "[SLA ALERT] level={:?} op={:?} ts={} msg={}",
719 alert.level, alert.operation, alert.timestamp, alert.message
720 );
721 Ok(())
722 }
723}
724
725pub struct InMemoryAlertHook {
737 identifier: String,
738 sent: RwLock<Vec<Alert>>,
739}
740
741impl InMemoryAlertHook {
742 pub fn new(identifier: String) -> Self {
744 Self {
745 identifier,
746 sent: RwLock::new(Vec::new()),
747 }
748 }
749
750 pub fn sent_alerts(&self) -> Vec<Alert> {
752 self.sent
753 .read()
754 .map(|guard| guard.clone())
755 .unwrap_or_default()
756 }
757
758 pub fn identifier(&self) -> &str {
760 &self.identifier
761 }
762
763 #[deprecated(since = "1.2.0", note = "use `identifier()` instead; this hook does not perform HTTP")]
769 pub fn url(&self) -> &str {
770 &self.identifier
771 }
772}
773
774impl AlertHook for InMemoryAlertHook {
775 fn notify(&self, alert: &Alert) -> Result<(), String> {
776 match self.sent.write() {
777 Ok(mut guard) => {
778 guard.push(alert.clone());
779 Ok(())
780 }
781 Err(e) => Err(format!("in-memory alert hook lock poisoned: {e}")),
782 }
783 }
784}
785
786struct OperationStats {
791 latency: LatencyHistogram,
792 total_count: usize,
793 error_count: usize,
794}
795
796impl OperationStats {
797 fn new() -> Self {
798 Self {
799 latency: LatencyHistogram::new(Vec::new()),
800 total_count: 0,
801 error_count: 0,
802 }
803 }
804
805 fn observe(&mut self, duration: Duration, success: bool) {
806 self.latency.record(duration);
807 self.total_count += 1;
808 if !success {
809 self.error_count += 1;
810 }
811 }
812
813 fn error_rate(&self) -> f64 {
814 if self.total_count == 0 {
815 0.0
816 } else {
817 self.error_count as f64 / self.total_count as f64
818 }
819 }
820}
821
822#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
824pub struct SlaReport {
825 pub p50_ms: f64,
827 pub p95_ms: f64,
829 pub p99_ms: f64,
831 pub error_rate: f64,
833 pub total_count: usize,
835 pub slo_target: f64,
837 pub error_budget_remaining: f64,
839 pub saturation: f64,
841}
842
843pub struct SlaMonitor {
848 slo_target: f64,
849 operations: RwLock<HashMap<String, OperationStats>>,
850}
851
852impl SlaMonitor {
853 pub fn new(slo_target: f64) -> Self {
854 Self {
855 slo_target,
856 operations: RwLock::new(HashMap::new()),
857 }
858 }
859
860 pub fn observe(&self, operation: &str, duration: Duration, success: bool) {
865 if let Ok(mut ops) = self.operations.write() {
866 let stats = ops
867 .entry(operation.to_string())
868 .or_insert_with(OperationStats::new);
869 stats.observe(duration, success);
870 }
871 }
872
873 pub fn report(&self, operation: &str) -> Option<SlaReport> {
874 let ops = self.operations.read().ok()?;
875 let stats = ops.get(operation)?;
876 let error_rate = stats.error_rate();
877 let error_budget = 1.0 - self.slo_target;
878 let (saturation, error_budget_remaining) = if error_budget <= 0.0 {
879 if error_rate == 0.0 {
881 (0.0, 1.0)
882 } else {
883 (1.0, 0.0)
884 }
885 } else {
886 let sat = (error_rate / error_budget).clamp(0.0, 1.0);
887 (sat, 1.0 - sat)
888 };
889 let ms = |p: f64| {
890 stats
891 .latency
892 .percentile(p)
893 .map(|d| d.as_nanos() as f64 / 1_000_000.0)
894 .unwrap_or(0.0)
895 };
896 Some(SlaReport {
897 p50_ms: ms(50.0),
898 p95_ms: ms(95.0),
899 p99_ms: ms(99.0),
900 error_rate,
901 total_count: stats.total_count,
902 slo_target: self.slo_target,
903 error_budget_remaining,
904 saturation,
905 })
906 }
907
908 pub fn operations(&self) -> Vec<String> {
909 self.operations
910 .read()
911 .map(|ops| ops.keys().cloned().collect())
912 .unwrap_or_default()
913 }
914}
915
916#[cfg(test)]
917mod tests {
918 use super::*;
919
920 #[test]
921 fn test_span_new() {
922 let span = Span::new("trace1", "span1", "operation1");
923 assert_eq!(span.trace_id, "trace1");
924 assert_eq!(span.span_id, "span1");
925 assert_eq!(span.operation_name, "operation1");
926 assert!(span.end_time.is_none());
927 }
928
929 #[test]
930 fn test_span_with_parent() {
931 let span = Span::new("trace1", "span1", "op").with_parent("parent1");
932 assert_eq!(span.parent_id, Some("parent1".to_string()));
933 }
934
935 #[test]
936 fn test_span_with_service() {
937 let span = Span::new("trace1", "span1", "op").with_service("my-service");
938 assert_eq!(span.service_name, "my-service");
939 }
940
941 #[test]
942 fn test_span_with_tag() {
943 let span = Span::new("trace1", "span1", "op").with_tag("key", "value");
944 assert_eq!(span.tags.get("key"), Some(&"value".to_string()));
945 }
946
947 #[test]
948 fn test_span_finish() {
949 let mut span = Span::new("trace1", "span1", "op");
950 span.finish();
951 assert!(span.end_time.is_some());
952 assert!(span.duration().is_some());
953 }
954
955 #[test]
956 fn test_span_add_log() {
957 let mut span = Span::new("trace1", "span1", "op");
958 span.add_log("test log");
959 assert_eq!(span.logs.len(), 1);
960 assert_eq!(span.logs[0].message, "test log");
961 }
962
963 #[test]
964 fn test_tracer_new() {
965 let tracer = SzTracer::new("test-service");
966 assert!(tracer.get_spans().is_empty());
967 }
968
969 #[test]
970 fn test_tracer_start_span() {
971 let tracer = SzTracer::new("test-service");
972 let span = tracer.start_span("test-operation");
973 assert_eq!(span.operation_name, "test-operation");
974 }
975
976 #[test]
977 fn test_tracer_end_span() {
978 let tracer = SzTracer::new("test-service");
979 let span = tracer.start_span("test-operation");
980 tracer.end_span(span);
981
982 let spans = tracer.get_spans();
983 assert_eq!(spans.len(), 1);
984 }
985
986 #[test]
987 fn test_tracer_inject() {
988 let tracer = SzTracer::new("test-service");
989 let span = tracer.start_span("test");
990 let headers = tracer.inject(&span);
991
992 let tp = headers
994 .get("traceparent")
995 .expect("traceparent header must be present");
996 let parts: Vec<&str> = tp.split('-').collect();
998 assert_eq!(parts.len(), 4);
999 assert_eq!(parts[0], "00"); assert_eq!(parts[1], span.trace_id);
1001 assert_eq!(parts[2], span.span_id);
1002 assert_eq!(parts[3], "01"); }
1004
1005 #[test]
1006 fn test_tracer_extract() {
1007 let tracer = SzTracer::new("test-service");
1008 let mut headers = HashMap::new();
1009 let trace_id = "0af7651916cd43dd8448eb211c80319c";
1011 let span_id = "b7ad6b7169203331";
1012 headers.insert(
1013 "traceparent".to_string(),
1014 format!("00-{}-{}-01", trace_id, span_id),
1015 );
1016
1017 let span = tracer.extract(&headers);
1018 assert!(span.is_some());
1019 let span = span.unwrap();
1020 assert_eq!(span.trace_id, trace_id);
1021 assert_eq!(span.span_id, span_id);
1022 }
1023
1024 #[test]
1025 fn test_tracer_extract_legacy_headers() {
1026 let tracer = SzTracer::new("test-service");
1028 let mut headers = HashMap::new();
1029 headers.insert("trace-id".to_string(), "trace123".to_string());
1030 headers.insert("span-id".to_string(), "span456".to_string());
1031
1032 let span = tracer.extract(&headers);
1033 assert!(span.is_some());
1034 let span = span.unwrap();
1035 assert_eq!(span.trace_id, "trace123");
1036 assert_eq!(span.span_id, "span456");
1037 }
1038
1039 #[test]
1040 fn test_tracer_extract_missing_headers() {
1041 let tracer = SzTracer::new("test-service");
1042 let headers = HashMap::new();
1043 let span = tracer.extract(&headers);
1044 assert!(span.is_none());
1045 }
1046
1047 #[test]
1050 fn test_parse_traceparent_valid() {
1051 let valid = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
1052 let span = SzTracer::parse_traceparent(valid).expect("valid traceparent must parse");
1053 assert_eq!(span.trace_id, "0af7651916cd43dd8448eb211c80319c");
1054 assert_eq!(span.span_id, "b7ad6b7169203331");
1055 }
1056
1057 #[test]
1058 fn test_parse_traceparent_invalid_version() {
1059 assert!(SzTracer::parse_traceparent(
1061 "0-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
1062 )
1063 .is_none());
1064 assert!(SzTracer::parse_traceparent(
1066 "xy-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
1067 )
1068 .is_none());
1069 }
1070
1071 #[test]
1072 fn test_parse_traceparent_invalid_trace_id_length() {
1073 assert!(SzTracer::parse_traceparent("00-short-b7ad6b7169203331-01").is_none());
1075 }
1076
1077 #[test]
1078 fn test_parse_traceparent_invalid_span_id_length() {
1079 assert!(
1081 SzTracer::parse_traceparent("00-0af7651916cd43dd8448eb211c80319c-short-01").is_none()
1082 );
1083 }
1084
1085 #[test]
1086 fn test_parse_traceparent_all_zeros_trace_id_rejected() {
1087 let all_zero = "00-00000000000000000000000000000000-b7ad6b7169203331-01";
1089 assert!(SzTracer::parse_traceparent(all_zero).is_none());
1090 }
1091
1092 #[test]
1093 fn test_parse_traceparent_all_zeros_span_id_rejected() {
1094 let all_zero = "00-0af7651916cd43dd8448eb211c80319c-0000000000000000-01";
1096 assert!(SzTracer::parse_traceparent(all_zero).is_none());
1097 }
1098
1099 #[test]
1100 fn test_parse_traceparent_invalid_flags() {
1101 assert!(SzTracer::parse_traceparent(
1103 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-xyz"
1104 )
1105 .is_none());
1106 }
1107
1108 #[test]
1109 fn test_parse_traceparent_wrong_part_count() {
1110 assert!(SzTracer::parse_traceparent(
1112 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331"
1113 )
1114 .is_none());
1115 assert!(SzTracer::parse_traceparent(
1116 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01-extra"
1117 )
1118 .is_none());
1119 }
1120
1121 #[test]
1122 fn test_inject_legacy_preserves_old_format() {
1123 let tracer = SzTracer::new("svc");
1124 let span = tracer.start_span("op");
1125 let headers = tracer.inject_legacy(&span);
1126
1127 assert_eq!(headers.get("trace-id"), Some(&span.trace_id.to_string()));
1128 assert_eq!(headers.get("span-id"), Some(&span.span_id.to_string()));
1129 assert!(!headers.contains_key("parent-span-id"));
1130 }
1131
1132 #[test]
1133 fn test_extract_legacy_preserves_old_format() {
1134 let tracer = SzTracer::new("svc");
1135 let mut headers = HashMap::new();
1136 headers.insert("trace-id".to_string(), "abc".to_string());
1137 headers.insert("span-id".to_string(), "def".to_string());
1138
1139 let span = tracer.extract_legacy(&headers).expect("legacy extract");
1140 assert_eq!(span.trace_id, "abc");
1141 assert_eq!(span.span_id, "def");
1142 }
1143
1144 #[test]
1145 fn test_w3c_traceparent_roundtrip_preserves_ids() {
1146 let tracer = SzTracer::new("svc");
1148 let original = tracer.start_span("roundtrip");
1149 let headers = tracer.inject(&original);
1150
1151 let extracted = tracer.extract(&headers).expect("roundtrip extract");
1152 assert_eq!(extracted.trace_id(), original.trace_id());
1153 assert_eq!(extracted.span_id(), original.span_id());
1154 assert!(extracted.parent_id().is_none());
1155 }
1156
1157 #[test]
1158 fn test_w3c_prefers_traceparent_over_legacy() {
1159 let tracer = SzTracer::new("svc");
1161 let mut headers = HashMap::new();
1162 headers.insert(
1163 "traceparent".to_string(),
1164 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_string(),
1165 );
1166 headers.insert("trace-id".to_string(), "legacy-trace".to_string());
1167 headers.insert("span-id".to_string(), "legacy-span".to_string());
1168
1169 let span = tracer.extract(&headers).expect("extract");
1170 assert_eq!(span.trace_id, "0af7651916cd43dd8448eb211c80319c");
1171 assert_eq!(span.span_id, "b7ad6b7169203331");
1172 }
1173
1174 #[test]
1175 fn test_w3c_falls_back_to_legacy_on_invalid_traceparent() {
1176 let tracer = SzTracer::new("svc");
1178 let mut headers = HashMap::new();
1179 headers.insert("traceparent".to_string(), "invalid-format".to_string());
1180 headers.insert("trace-id".to_string(), "legacy-trace".to_string());
1181 headers.insert("span-id".to_string(), "legacy-span".to_string());
1182
1183 let span = tracer
1184 .extract(&headers)
1185 .expect("should fall back to legacy");
1186 assert_eq!(span.trace_id, "legacy-trace");
1187 assert_eq!(span.span_id, "legacy-span");
1188 }
1189
1190 #[test]
1191 fn test_otel_tracer() {
1192 let tracer = OtelTracer::new("test-service");
1193 let span = tracer.start_span("test-operation");
1194 assert_eq!(span.operation_name, "test-operation");
1195 }
1196
1197 #[test]
1198 fn test_otel_tracer_is_a_sz_tracer_wrapper_not_a_real_otel_sdk() {
1199 let tracer = OtelTracer::new("svc");
1203 assert!(tracer.inner().get_spans().is_empty());
1204
1205 let span = tracer.start_span("op");
1206 assert_eq!(span.service_name(), "svc");
1207 tracer.end_span(span);
1208
1209 let spans = tracer.inner().get_spans();
1210 assert_eq!(spans.len(), 1);
1211 assert_eq!(spans[0].operation_name(), "op");
1212 assert!(spans[0].end_time.is_some());
1213 }
1214
1215 #[test]
1216 fn test_otel_tracer_inject_extract_roundtrip_preserves_ids() {
1217 let tracer = OtelTracer::new("svc");
1219 let original = tracer.start_span("roundtrip");
1220 let headers = tracer.inject(&original);
1221
1222 let tp = headers
1224 .get("traceparent")
1225 .expect("traceparent must be present");
1226 assert!(tp.contains(original.trace_id()));
1227 assert!(tp.contains(original.span_id()));
1228 assert!(!headers.contains_key("parent-span-id"));
1229
1230 let extracted = tracer.extract(&headers).expect("extract should round-trip");
1231 assert_eq!(extracted.trace_id(), original.trace_id());
1232 assert_eq!(extracted.span_id(), original.span_id());
1233 assert!(extracted.parent_id().is_none());
1234 }
1235
1236 #[test]
1237 fn test_otel_tracer_preserves_parent_id_through_roundtrip() {
1238 let tracer = OtelTracer::new("svc");
1239 let parent = tracer.start_span("parent");
1240 let child = tracer
1241 .start_span("child")
1242 .with_parent(parent.span_id().to_string());
1243 let headers = tracer.inject(&child);
1244
1245 assert_eq!(
1247 headers.get("parent-span-id"),
1248 Some(&parent.span_id().to_string())
1249 );
1250
1251 let extracted = tracer.extract(&headers).expect("extract should round-trip");
1252 assert_eq!(extracted.parent_id(), Some(parent.span_id()));
1253 }
1254
1255 #[test]
1256 fn test_otel_tracer_extract_returns_none_without_required_headers() {
1257 let tracer = OtelTracer::new("svc");
1258 let headers: HashMap<String, String> = HashMap::new();
1259 assert!(tracer.extract(&headers).is_none());
1260
1261 let mut partial = HashMap::new();
1263 partial.insert("trace-id".to_string(), "abc".to_string());
1264 assert!(tracer.extract(&partial).is_none());
1266
1267 let mut bad_w3c = HashMap::new();
1269 bad_w3c.insert("traceparent".to_string(), "garbage".to_string());
1270 assert!(tracer.extract(&bad_w3c).is_none());
1271 }
1272
1273 #[test]
1274 fn test_otel_tracer_generated_ids_have_correct_length() {
1275 let trace_id = SzTracer::generate_trace_id();
1280 let span_id = SzTracer::generate_span_id();
1281 assert_eq!(trace_id.len(), 32, "trace_id must be 32 hex chars");
1282 assert_eq!(span_id.len(), 16, "span_id must be 16 hex chars");
1283
1284 assert!(trace_id.chars().all(|c| c.is_ascii_hexdigit()));
1286 assert!(span_id.chars().all(|c| c.is_ascii_hexdigit()));
1287 }
1288
1289 #[test]
1290 fn test_span_accessors() {
1291 let span = Span::new("trace1", "span1", "test-op")
1292 .with_service("svc")
1293 .with_tag("k", "v");
1294
1295 assert_eq!(span.trace_id(), "trace1");
1296 assert_eq!(span.span_id(), "span1");
1297 assert_eq!(span.operation_name(), "test-op");
1298 assert_eq!(span.service_name(), "svc");
1299 assert_eq!(span.tags().get("k"), Some(&"v".to_string()));
1300 }
1301
1302 #[test]
1303 fn test_generate_ids() {
1304 let trace_id = SzTracer::generate_trace_id();
1305 let span_id = SzTracer::generate_span_id();
1306
1307 assert_eq!(trace_id.len(), 32);
1308 assert_eq!(span_id.len(), 16);
1309 }
1310
1311 #[test]
1312 fn test_tracer_clear() {
1313 let tracer = SzTracer::new("test-service");
1314
1315 let span = tracer.start_span("op1");
1316 tracer.end_span(span);
1317 let span = tracer.start_span("op2");
1318 tracer.end_span(span);
1319
1320 assert_eq!(tracer.get_spans().len(), 2);
1321
1322 tracer.clear();
1323 assert!(tracer.get_spans().is_empty());
1324 }
1325
1326 #[test]
1329 fn test_latency_histogram_new_empty() {
1330 let hist = LatencyHistogram::new(vec![
1331 Duration::from_millis(10),
1332 Duration::from_millis(100),
1333 Duration::from_millis(1000),
1334 ]);
1335 assert_eq!(hist.count(), 0);
1336 assert!(hist.percentile(50.0).is_none());
1337 assert!(hist.mean().is_none());
1338 }
1339
1340 #[test]
1341 fn test_latency_histogram_record_single() {
1342 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1343 hist.record(Duration::from_millis(50));
1344 assert_eq!(hist.count(), 1);
1345 assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(50)));
1346 assert_eq!(hist.mean(), Some(Duration::from_millis(50)));
1347 }
1348
1349 #[test]
1350 fn test_latency_histogram_percentile_p50_sorted() {
1351 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1352 for ms in [10, 20, 30, 40, 50] {
1353 hist.record(Duration::from_millis(ms));
1354 }
1355 assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(30)));
1357 }
1358
1359 #[test]
1360 fn test_latency_histogram_percentile_p95_high_value() {
1361 let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1362 for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1363 hist.record(Duration::from_millis(ms));
1364 }
1365 let p95 = hist.percentile(95.0).expect("p95 must exist");
1367 assert!(p95 >= Duration::from_millis(9));
1368 }
1369
1370 #[test]
1371 fn test_latency_histogram_percentile_p99_max_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 p99 = hist.percentile(99.0).expect("p99 must exist");
1377 assert_eq!(p99, Duration::from_millis(100));
1378 }
1379
1380 #[test]
1381 fn test_latency_histogram_percentile_p0_min_value() {
1382 let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1383 for ms in [10, 20, 30] {
1384 hist.record(Duration::from_millis(ms));
1385 }
1386 assert_eq!(hist.percentile(0.0), Some(Duration::from_millis(10)));
1387 }
1388
1389 #[test]
1390 fn test_latency_histogram_percentile_p100_max_value() {
1391 let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1392 for ms in [10, 20, 30] {
1393 hist.record(Duration::from_millis(ms));
1394 }
1395 assert_eq!(hist.percentile(100.0), Some(Duration::from_millis(30)));
1396 }
1397
1398 #[test]
1399 fn test_latency_histogram_percentile_empty_returns_none() {
1400 let hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1401 assert!(hist.percentile(50.0).is_none());
1402 }
1403
1404 #[test]
1405 fn test_latency_histogram_percentile_out_of_range_returns_none() {
1406 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1407 hist.record(Duration::from_millis(50));
1408 assert!(hist.percentile(-1.0).is_none());
1409 assert!(hist.percentile(101.0).is_none());
1410 }
1411
1412 #[test]
1413 fn test_latency_histogram_count() {
1414 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1415 assert_eq!(hist.count(), 0);
1416 hist.record(Duration::from_millis(10));
1417 hist.record(Duration::from_millis(20));
1418 hist.record(Duration::from_millis(30));
1419 assert_eq!(hist.count(), 3);
1420 }
1421
1422 #[test]
1423 fn test_latency_histogram_mean_multiple() {
1424 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1425 for ms in [10, 20, 30, 40, 50] {
1426 hist.record(Duration::from_millis(ms));
1427 }
1428 assert_eq!(hist.mean(), Some(Duration::from_millis(30)));
1430 }
1431
1432 #[test]
1433 fn test_latency_histogram_record_unsorted_input_stays_sorted() {
1434 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1435 hist.record(Duration::from_millis(50));
1436 hist.record(Duration::from_millis(10));
1437 hist.record(Duration::from_millis(30));
1438 assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(30)));
1440 }
1441
1442 #[test]
1445 fn test_error_rate_counter_new_empty() {
1446 let counter = ErrorRateCounter::new(Duration::from_secs(60));
1447 assert_eq!(counter.total(), 0);
1448 assert_eq!(counter.errors(), 0);
1449 assert_eq!(counter.rate(), 0.0);
1450 }
1451
1452 #[test]
1453 fn test_error_rate_counter_all_success_rate_zero() {
1454 let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1455 for _ in 0..10 {
1456 counter.record(true);
1457 }
1458 assert_eq!(counter.total(), 10);
1459 assert_eq!(counter.errors(), 0);
1460 assert_eq!(counter.rate(), 0.0);
1461 }
1462
1463 #[test]
1464 fn test_error_rate_counter_all_failures_rate_one() {
1465 let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1466 for _ in 0..10 {
1467 counter.record(false);
1468 }
1469 assert_eq!(counter.total(), 10);
1470 assert_eq!(counter.errors(), 10);
1471 assert_eq!(counter.rate(), 1.0);
1472 }
1473
1474 #[test]
1475 fn test_error_rate_counter_mixed_rate() {
1476 let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1477 for _ in 0..7 {
1479 counter.record(true);
1480 }
1481 for _ in 0..3 {
1482 counter.record(false);
1483 }
1484 assert_eq!(counter.total(), 10);
1485 assert_eq!(counter.errors(), 3);
1486 let rate = counter.rate();
1487 assert!((rate - 0.3).abs() < 1e-9, "expected 0.3, got {rate}");
1488 }
1489
1490 #[test]
1491 fn test_error_rate_counter_empty_rate_is_zero() {
1492 let counter = ErrorRateCounter::new(Duration::from_secs(60));
1493 assert_eq!(counter.rate(), 0.0);
1495 }
1496
1497 #[test]
1498 fn test_error_rate_counter_window_expires_old_samples() {
1499 let mut counter = ErrorRateCounter::new(Duration::from_millis(100));
1501 counter.record(false);
1502 counter.record(false);
1503 std::thread::sleep(Duration::from_millis(120));
1505 counter.record(true);
1506 assert_eq!(counter.total(), 1);
1508 assert_eq!(counter.errors(), 0);
1509 assert_eq!(counter.rate(), 0.0);
1510 }
1511
1512 #[test]
1515 fn test_error_budget_new_is_full() {
1516 let budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1517 assert!((budget.remaining() - 1.0).abs() < 1e-9);
1518 assert!(!budget.is_exhausted());
1519 }
1520
1521 #[test]
1522 fn test_error_budget_consume_reduces_remaining() {
1523 let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1524 budget.consume(1);
1526 assert!((budget.remaining() - 0.999).abs() < 1e-9);
1528 assert!(!budget.is_exhausted());
1529 }
1530
1531 #[test]
1532 fn test_error_budget_exhausted_at_capacity() {
1533 let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1534 budget.consume(1000);
1536 assert_eq!(budget.remaining(), 0.0);
1537 assert!(budget.is_exhausted());
1538 }
1539
1540 #[test]
1541 fn test_error_budget_over_consume_clamps_to_zero() {
1542 let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1543 budget.consume(2000);
1544 assert_eq!(budget.remaining(), 0.0);
1545 assert!(budget.is_exhausted());
1546 }
1547
1548 #[test]
1549 fn test_error_budget_zero_errors_returns_one() {
1550 let budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1551 assert_eq!(budget.remaining(), 1.0);
1552 assert!(!budget.is_exhausted());
1553 }
1554
1555 #[test]
1556 fn test_error_budget_window_refills_after_expiry() {
1557 let mut budget = ErrorBudget::new(0.5, Duration::from_millis(100));
1559 budget.consume(2);
1560 assert!(budget.is_exhausted());
1561 std::thread::sleep(Duration::from_millis(120));
1563 assert!((budget.remaining() - 1.0).abs() < 1e-9);
1564 assert!(!budget.is_exhausted());
1565 }
1566
1567 #[test]
1568 fn test_error_budget_slo_one_means_no_errors_allowed() {
1569 let mut budget = ErrorBudget::new(1.0, Duration::from_secs(60));
1571 assert_eq!(budget.remaining(), 1.0);
1572 budget.consume(1);
1573 assert_eq!(budget.remaining(), 0.0);
1574 assert!(budget.is_exhausted());
1575 }
1576
1577 #[test]
1578 fn test_error_budget_multiple_consumes_accumulate() {
1579 let mut budget = ErrorBudget::new(0.99, Duration::from_secs(60));
1580 budget.consume(30);
1582 assert!((budget.remaining() - 0.7).abs() < 1e-9);
1583 budget.consume(30);
1584 assert!((budget.remaining() - 0.4).abs() < 1e-9);
1585 budget.consume(40);
1586 assert_eq!(budget.remaining(), 0.0);
1587 assert!(budget.is_exhausted());
1588 }
1589
1590 #[test]
1593 fn test_alert_level_variants_exist() {
1594 let info = AlertLevel::Info;
1595 let warning = AlertLevel::Warning;
1596 let critical = AlertLevel::Critical;
1597 assert_ne!(format!("{info:?}"), format!("{warning:?}"));
1599 assert_ne!(format!("{warning:?}"), format!("{critical:?}"));
1600 assert_ne!(format!("{info:?}"), format!("{critical:?}"));
1601 }
1602
1603 #[test]
1604 fn test_alert_construction_with_all_fields() {
1605 let ts = Utc::now();
1606 let alert = Alert {
1607 level: AlertLevel::Critical,
1608 message: "p99 latency exceeded budget".to_string(),
1609 timestamp: ts,
1610 operation: Some("query_user".to_string()),
1611 };
1612 assert_eq!(alert.level, AlertLevel::Critical);
1613 assert_eq!(alert.message, "p99 latency exceeded budget");
1614 assert_eq!(alert.timestamp, ts);
1615 assert_eq!(alert.operation.as_deref(), Some("query_user"));
1616 }
1617
1618 #[test]
1619 fn test_alert_construction_without_operation() {
1620 let alert = Alert {
1621 level: AlertLevel::Info,
1622 message: "system healthy".to_string(),
1623 timestamp: Utc::now(),
1624 operation: None,
1625 };
1626 assert!(alert.operation.is_none());
1627 }
1628
1629 #[test]
1630 fn test_alert_implements_clone_debug() {
1631 let alert = Alert {
1632 level: AlertLevel::Warning,
1633 message: "approaching budget".to_string(),
1634 timestamp: Utc::now(),
1635 operation: Some("op".to_string()),
1636 };
1637 let cloned = alert.clone();
1638 assert_eq!(cloned.level, alert.level);
1639 assert_eq!(cloned.message, alert.message);
1640 let debug_str = format!("{alert:?}");
1642 assert!(debug_str.contains("approaching budget"), "debug output missing message: {}", debug_str);
1643 assert!(debug_str.contains("Warning"), "debug output missing level: {}", debug_str);
1644 }
1645
1646 #[test]
1649 fn test_saturation_gauge_new_starts_unsaturated() {
1650 let gauge = SaturationGauge::new(0.8);
1651 assert!(!gauge.is_saturated());
1652 assert!(gauge.check_alert().is_none());
1653 }
1654
1655 #[test]
1656 fn test_saturation_gauge_set_below_threshold_not_saturated() {
1657 let mut gauge = SaturationGauge::new(0.8);
1658 gauge.set(0.5);
1659 assert!(!gauge.is_saturated());
1660 assert!(gauge.check_alert().is_none());
1661 }
1662
1663 #[test]
1664 fn test_saturation_gauge_set_at_threshold_is_saturated() {
1665 let mut gauge = SaturationGauge::new(0.8);
1666 gauge.set(0.8);
1667 assert!(gauge.is_saturated());
1668 }
1669
1670 #[test]
1671 fn test_saturation_gauge_set_above_threshold_is_saturated() {
1672 let mut gauge = SaturationGauge::new(0.8);
1673 gauge.set(0.95);
1674 assert!(gauge.is_saturated());
1675 }
1676
1677 #[test]
1678 fn test_saturation_gauge_check_alert_returns_critical_when_saturated() {
1679 let mut gauge = SaturationGauge::new(0.8);
1680 gauge.set(0.95);
1681 let alert = gauge.check_alert().expect("alert must fire when saturated");
1682 assert_eq!(alert.level, AlertLevel::Critical);
1683 assert!(!alert.message.is_empty());
1684 assert!(alert.operation.is_none()); }
1686
1687 #[test]
1688 fn test_saturation_gauge_check_alert_returns_none_when_not_saturated() {
1689 let mut gauge = SaturationGauge::new(0.8);
1690 gauge.set(0.3);
1691 assert!(gauge.check_alert().is_none());
1692 }
1693
1694 #[test]
1695 fn test_saturation_gauge_set_zero_not_saturated() {
1696 let mut gauge = SaturationGauge::new(0.8);
1697 gauge.set(0.0);
1698 assert!(!gauge.is_saturated());
1699 }
1700
1701 #[test]
1702 fn test_saturation_gauge_set_one_saturated() {
1703 let mut gauge = SaturationGauge::new(0.5);
1704 gauge.set(1.0);
1705 assert!(gauge.is_saturated());
1706 }
1707
1708 #[test]
1709 fn test_saturation_gauge_threshold_zero_always_saturated() {
1710 let mut gauge = SaturationGauge::new(0.0);
1711 gauge.set(0.0);
1712 assert!(gauge.is_saturated());
1715 }
1716
1717 fn sample_alert(level: AlertLevel, op: Option<&str>) -> Alert {
1720 Alert {
1721 level,
1722 message: "test alert".to_string(),
1723 timestamp: Utc::now(),
1724 operation: op.map(str::to_string),
1725 }
1726 }
1727
1728 #[test]
1729 fn test_log_alert_hook_notify_returns_ok() {
1730 let hook = LogAlertHook::new();
1731 let alert = sample_alert(AlertLevel::Warning, Some("op"));
1732 let result = hook.notify(&alert);
1733 assert!(result.is_ok());
1734 }
1735
1736 #[test]
1737 fn test_log_alert_hook_notify_critical_succeeds() {
1738 let hook = LogAlertHook::new();
1739 let alert = sample_alert(AlertLevel::Critical, None);
1740 let result = hook.notify(&alert);
1741 assert!(result.is_ok());
1742 }
1743
1744 #[test]
1745 fn test_log_alert_hook_implements_send_sync() {
1746 fn assert_send_sync<T: Send + Sync>() {}
1747 assert_send_sync::<LogAlertHook>();
1748 }
1749
1750 #[test]
1751 fn test_webhook_alert_hook_new_starts_empty() {
1752 let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1753 assert!(hook.sent_alerts().is_empty());
1754 }
1755
1756 #[test]
1757 fn test_webhook_alert_hook_notify_stores_alert() {
1758 let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1759 let alert = sample_alert(AlertLevel::Critical, Some("query_user"));
1760 hook.notify(&alert).expect("notify must succeed");
1761 let sent = hook.sent_alerts();
1762 assert_eq!(sent.len(), 1);
1763 assert_eq!(sent[0], alert);
1764 }
1765
1766 #[test]
1767 fn test_webhook_alert_hook_multiple_notifications_accumulate() {
1768 let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1769 let a1 = sample_alert(AlertLevel::Info, None);
1770 let a2 = sample_alert(AlertLevel::Warning, Some("op1"));
1771 let a3 = sample_alert(AlertLevel::Critical, Some("op2"));
1772 hook.notify(&a1).unwrap();
1773 hook.notify(&a2).unwrap();
1774 hook.notify(&a3).unwrap();
1775 let sent = hook.sent_alerts();
1776 assert_eq!(sent.len(), 3);
1777 assert_eq!(sent[0], a1);
1778 assert_eq!(sent[1], a2);
1779 assert_eq!(sent[2], a3);
1780 }
1781
1782 #[test]
1783 fn test_webhook_alert_hook_sent_alerts_returns_clone() {
1784 let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1786 let alert = sample_alert(AlertLevel::Info, None);
1787 hook.notify(&alert).unwrap();
1788 let mut sent = hook.sent_alerts();
1789 sent.clear();
1790 assert_eq!(hook.sent_alerts().len(), 1);
1792 }
1793
1794 #[test]
1795 fn test_webhook_alert_hook_implements_send_sync() {
1796 fn assert_send_sync<T: Send + Sync>() {}
1797 assert_send_sync::<InMemoryAlertHook>();
1798 }
1799
1800 #[test]
1801 fn test_alert_hook_trait_object_dispatch() {
1802 let hooks: Vec<Box<dyn AlertHook>> = vec![
1804 Box::new(LogAlertHook::new()),
1805 Box::new(InMemoryAlertHook::new(
1806 "https://example.com/hook".to_string(),
1807 )),
1808 ];
1809 let alert = sample_alert(AlertLevel::Critical, Some("op"));
1810 for hook in &hooks {
1811 assert!(hook.notify(&alert).is_ok());
1812 }
1813 let webhook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1816 webhook.notify(&alert).unwrap();
1817 assert_eq!(webhook.sent_alerts().len(), 1);
1818 }
1819
1820 #[test]
1823 fn test_sla_monitor_new_empty() {
1824 let monitor = SlaMonitor::new(0.999);
1825 assert!(monitor.operations().is_empty());
1826 }
1827
1828 #[test]
1829 fn test_sla_monitor_observe_creates_operation() {
1830 let monitor = SlaMonitor::new(0.999);
1831 monitor.observe("query", Duration::from_millis(50), true);
1832 let ops = monitor.operations();
1833 assert_eq!(ops, vec!["query".to_string()]);
1834 }
1835
1836 #[test]
1837 fn test_sla_monitor_report_unknown_returns_none() {
1838 let monitor = SlaMonitor::new(0.999);
1839 assert!(monitor.report("unknown").is_none());
1840 }
1841
1842 #[test]
1843 fn test_sla_monitor_report_basic_stats_all_success() {
1844 let monitor = SlaMonitor::new(0.999);
1845 for ms in [10, 20, 30, 40, 50] {
1846 monitor.observe("op", Duration::from_millis(ms), true);
1847 }
1848 let report = monitor.report("op").expect("report must exist");
1849 assert_eq!(report.total_count, 5);
1850 assert_eq!(report.error_rate, 0.0);
1851 assert!((report.slo_target - 0.999).abs() < 1e-9);
1852 assert!((report.p50_ms - 30.0).abs() < 1e-9);
1854 assert!((report.error_budget_remaining - 1.0).abs() < 1e-9);
1856 assert!((report.saturation - 0.0).abs() < 1e-9);
1857 }
1858
1859 #[test]
1860 fn test_sla_monitor_report_with_errors_over_budget() {
1861 let monitor = SlaMonitor::new(0.999);
1862 for _ in 0..8 {
1864 monitor.observe("op", Duration::from_millis(10), true);
1865 }
1866 for _ in 0..2 {
1867 monitor.observe("op", Duration::from_millis(10), false);
1868 }
1869 let report = monitor.report("op").expect("report must exist");
1870 assert_eq!(report.total_count, 10);
1871 assert!((report.error_rate - 0.2).abs() < 1e-9);
1872 assert!((report.saturation - 1.0).abs() < 1e-9);
1875 assert!((report.error_budget_remaining - 0.0).abs() < 1e-9);
1876 }
1877
1878 #[test]
1879 fn test_sla_monitor_report_partial_budget() {
1880 let monitor = SlaMonitor::new(0.9);
1885 for i in 0..20 {
1886 monitor.observe("op", Duration::from_millis(i), i != 5);
1887 }
1888 let report = monitor.report("op").expect("report");
1889 assert!((report.error_rate - 0.05).abs() < 1e-9);
1890 assert!((report.saturation - 0.5).abs() < 1e-9);
1891 assert!((report.error_budget_remaining - 0.5).abs() < 1e-9);
1892 }
1893
1894 #[test]
1895 fn test_sla_monitor_operations_isolated() {
1896 let monitor = SlaMonitor::new(0.999);
1897 monitor.observe("op1", Duration::from_millis(10), true);
1898 monitor.observe("op2", Duration::from_millis(20), false);
1899 let mut ops = monitor.operations();
1900 ops.sort();
1901 assert_eq!(ops, vec!["op1".to_string(), "op2".to_string()]);
1902
1903 let r1 = monitor.report("op1").expect("op1 report");
1904 let r2 = monitor.report("op2").expect("op2 report");
1905 assert_eq!(r1.total_count, 1);
1906 assert_eq!(r2.total_count, 1);
1907 assert!((r1.error_rate - 0.0).abs() < 1e-9);
1908 assert!((r2.error_rate - 1.0).abs() < 1e-9);
1909 }
1910
1911 #[test]
1912 fn test_sla_monitor_p95_p99_high_percentile() {
1913 let monitor = SlaMonitor::new(0.999);
1914 for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1915 monitor.observe("op", Duration::from_millis(ms), true);
1916 }
1917 let report = monitor.report("op").expect("report");
1918 assert!((report.p95_ms - 100.0).abs() < 1e-9);
1920 assert!((report.p99_ms - 100.0).abs() < 1e-9);
1921 }
1922
1923 #[test]
1924 fn test_sla_monitor_observe_aggregates_multiple_calls() {
1925 let monitor = SlaMonitor::new(0.999);
1926 for _ in 0..100 {
1927 monitor.observe("op", Duration::from_millis(5), true);
1928 }
1929 let report = monitor.report("op").expect("report");
1930 assert_eq!(report.total_count, 100);
1931 assert!((report.p50_ms - 5.0).abs() < 1e-9);
1932 }
1933
1934 #[test]
1935 fn test_sla_monitor_implements_send_sync() {
1936 fn assert_send_sync<T: Send + Sync>() {}
1937 assert_send_sync::<SlaMonitor>();
1938 }
1939
1940 #[test]
1941 fn test_sla_monitor_concurrent_observe_thread_safe() {
1942 use std::sync::Arc;
1943 use std::thread;
1944
1945 let monitor = Arc::new(SlaMonitor::new(0.999));
1946 let mut handles = vec![];
1947
1948 for t in 0..4 {
1949 let m = Arc::clone(&monitor);
1950 handles.push(thread::spawn(move || {
1951 for i in 0..100 {
1952 m.observe("op", Duration::from_millis(i as u64), i % 10 != 0);
1954 }
1955 let op_name = format!("thread-{t}");
1957 m.observe(&op_name, Duration::from_millis(1), true);
1958 }));
1959 }
1960
1961 for h in handles {
1962 h.join().unwrap();
1963 }
1964
1965 let report = monitor.report("op").expect("op report");
1966 assert_eq!(report.total_count, 400);
1967 assert!((report.error_rate - 0.1).abs() < 1e-9);
1969
1970 let mut ops = monitor.operations();
1972 ops.sort();
1973 assert_eq!(ops.len(), 5);
1975 assert!(ops.contains(&"op".to_string()));
1976 }
1977
1978 #[test]
1979 fn test_sla_monitor_report_slo_one_with_no_errors() {
1980 let monitor = SlaMonitor::new(1.0);
1982 monitor.observe("op", Duration::from_millis(10), true);
1983 let report = monitor.report("op").expect("report");
1984 assert!((report.error_budget_remaining - 1.0).abs() < 1e-9);
1985 assert!((report.saturation - 0.0).abs() < 1e-9);
1986 }
1987
1988 #[test]
1989 fn test_sla_monitor_report_slo_one_with_errors() {
1990 let monitor = SlaMonitor::new(1.0);
1992 monitor.observe("op", Duration::from_millis(10), false);
1993 let report = monitor.report("op").expect("report");
1994 assert!((report.error_budget_remaining - 0.0).abs() < 1e-9);
1995 assert!((report.saturation - 1.0).abs() < 1e-9);
1996 }
1997
1998 #[test]
1999 fn test_sla_report_fields_are_public() {
2000 let monitor = SlaMonitor::new(0.999);
2002 monitor.observe("op", Duration::from_millis(10), true);
2003 let r = monitor.report("op").unwrap();
2004 let _p50: f64 = r.p50_ms;
2005 let _p95: f64 = r.p95_ms;
2006 let _p99: f64 = r.p99_ms;
2007 let _erate: f64 = r.error_rate;
2008 let _total: usize = r.total_count;
2009 let _slo: f64 = r.slo_target;
2010 let _ebr: f64 = r.error_budget_remaining;
2011 let _sat: f64 = r.saturation;
2012 }
2013}
2014
2015#[cfg(feature = "otlp")]
2037#[derive(Debug, Clone)]
2038pub struct OtlpConfig {
2039 pub endpoint: String,
2041 pub service_name: String,
2043 pub timeout_ms: u64,
2045}
2046
2047#[cfg(feature = "otlp")]
2048impl Default for OtlpConfig {
2049 fn default() -> Self {
2050 Self {
2051 endpoint: "http://localhost:4317".to_string(),
2052 service_name: "sz-orm".to_string(),
2053 timeout_ms: 5000,
2054 }
2055 }
2056}
2057
2058#[cfg(feature = "otlp")]
2080pub async fn init_otlp_exporter(config: OtlpConfig) -> Result<OtlpGuard, TracingError> {
2081 use opentelemetry_otlp::{SpanExporter, WithExportConfig};
2082 use opentelemetry_sdk::resource::Resource;
2083 use opentelemetry_sdk::runtime::Tokio;
2084 use opentelemetry_sdk::trace::TracerProvider;
2085 use std::time::Duration;
2086
2087 let exporter = SpanExporter::builder()
2088 .with_tonic()
2089 .with_endpoint(config.endpoint.clone())
2090 .with_timeout(Duration::from_millis(config.timeout_ms))
2091 .build()
2092 .map_err(|e| TracingError::OtlpInitFailed(format!("exporter build: {e}")))?;
2093
2094 let provider = TracerProvider::builder()
2095 .with_batch_exporter(exporter, Tokio)
2096 .with_resource(Resource::new_with_defaults([opentelemetry::KeyValue::new(
2097 "service.name",
2098 config.service_name.clone(),
2099 )]))
2100 .build();
2101
2102 opentelemetry::global::set_tracer_provider(provider.clone());
2104
2105 Ok(OtlpGuard { provider })
2106}
2107
2108#[cfg(feature = "otlp")]
2112pub struct OtlpGuard {
2113 provider: opentelemetry_sdk::trace::TracerProvider,
2114}
2115
2116#[cfg(feature = "otlp")]
2117impl Drop for OtlpGuard {
2118 fn drop(&mut self) {
2119 let _ = self.provider.shutdown();
2121 }
2122}