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 .expect("tracing spans read lock poisoned")
162 .clone()
163 }
164
165 pub fn clear(&self) {
166 self.spans
167 .write()
168 .map_err(|e| TracingError::Internal(e.to_string()))
169 .expect("tracing spans write lock poisoned")
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(
769 since = "1.2.0",
770 note = "use `identifier()` instead; this hook does not perform HTTP"
771 )]
772 pub fn url(&self) -> &str {
773 &self.identifier
774 }
775}
776
777impl AlertHook for InMemoryAlertHook {
778 fn notify(&self, alert: &Alert) -> Result<(), String> {
779 match self.sent.write() {
780 Ok(mut guard) => {
781 guard.push(alert.clone());
782 Ok(())
783 }
784 Err(e) => Err(format!("in-memory alert hook lock poisoned: {e}")),
785 }
786 }
787}
788
789struct OperationStats {
794 latency: LatencyHistogram,
795 total_count: usize,
796 error_count: usize,
797}
798
799impl OperationStats {
800 fn new() -> Self {
801 Self {
802 latency: LatencyHistogram::new(Vec::new()),
803 total_count: 0,
804 error_count: 0,
805 }
806 }
807
808 fn observe(&mut self, duration: Duration, success: bool) {
809 self.latency.record(duration);
810 self.total_count += 1;
811 if !success {
812 self.error_count += 1;
813 }
814 }
815
816 fn error_rate(&self) -> f64 {
817 if self.total_count == 0 {
818 0.0
819 } else {
820 self.error_count as f64 / self.total_count as f64
821 }
822 }
823}
824
825#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
827pub struct SlaReport {
828 pub p50_ms: f64,
830 pub p95_ms: f64,
832 pub p99_ms: f64,
834 pub error_rate: f64,
836 pub total_count: usize,
838 pub slo_target: f64,
840 pub error_budget_remaining: f64,
842 pub saturation: f64,
844}
845
846pub struct SlaMonitor {
851 slo_target: f64,
852 operations: RwLock<HashMap<String, OperationStats>>,
853}
854
855impl SlaMonitor {
856 pub fn new(slo_target: f64) -> Self {
857 Self {
858 slo_target,
859 operations: RwLock::new(HashMap::new()),
860 }
861 }
862
863 pub fn observe(&self, operation: &str, duration: Duration, success: bool) {
868 if let Ok(mut ops) = self.operations.write() {
869 let stats = ops
870 .entry(operation.to_string())
871 .or_insert_with(OperationStats::new);
872 stats.observe(duration, success);
873 }
874 }
875
876 pub fn report(&self, operation: &str) -> Option<SlaReport> {
877 let ops = self.operations.read().ok()?;
878 let stats = ops.get(operation)?;
879 let error_rate = stats.error_rate();
880 let error_budget = 1.0 - self.slo_target;
881 let (saturation, error_budget_remaining) = if error_budget <= 0.0 {
882 if error_rate == 0.0 {
884 (0.0, 1.0)
885 } else {
886 (1.0, 0.0)
887 }
888 } else {
889 let sat = (error_rate / error_budget).clamp(0.0, 1.0);
890 (sat, 1.0 - sat)
891 };
892 let ms = |p: f64| {
893 stats
894 .latency
895 .percentile(p)
896 .map(|d| d.as_nanos() as f64 / 1_000_000.0)
897 .unwrap_or(0.0)
898 };
899 Some(SlaReport {
900 p50_ms: ms(50.0),
901 p95_ms: ms(95.0),
902 p99_ms: ms(99.0),
903 error_rate,
904 total_count: stats.total_count,
905 slo_target: self.slo_target,
906 error_budget_remaining,
907 saturation,
908 })
909 }
910
911 pub fn operations(&self) -> Vec<String> {
912 self.operations
913 .read()
914 .map(|ops| ops.keys().cloned().collect())
915 .unwrap_or_default()
916 }
917}
918
919#[cfg(test)]
920mod tests {
921 use super::*;
922
923 #[test]
924 fn test_span_new() {
925 let span = Span::new("trace1", "span1", "operation1");
926 assert_eq!(span.trace_id, "trace1");
927 assert_eq!(span.span_id, "span1");
928 assert_eq!(span.operation_name, "operation1");
929 assert!(span.end_time.is_none());
930 }
931
932 #[test]
933 fn test_span_with_parent() {
934 let span = Span::new("trace1", "span1", "op").with_parent("parent1");
935 assert_eq!(span.parent_id, Some("parent1".to_string()));
936 }
937
938 #[test]
939 fn test_span_with_service() {
940 let span = Span::new("trace1", "span1", "op").with_service("my-service");
941 assert_eq!(span.service_name, "my-service");
942 }
943
944 #[test]
945 fn test_span_with_tag() {
946 let span = Span::new("trace1", "span1", "op").with_tag("key", "value");
947 assert_eq!(span.tags.get("key"), Some(&"value".to_string()));
948 }
949
950 #[test]
951 fn test_span_finish() {
952 let mut span = Span::new("trace1", "span1", "op");
953 span.finish();
954 assert!(span.end_time.is_some());
955 assert!(span.duration().is_some());
956 }
957
958 #[test]
959 fn test_span_add_log() {
960 let mut span = Span::new("trace1", "span1", "op");
961 span.add_log("test log");
962 assert_eq!(span.logs.len(), 1);
963 assert_eq!(span.logs[0].message, "test log");
964 }
965
966 #[test]
967 fn test_tracer_new() {
968 let tracer = SzTracer::new("test-service");
969 assert!(tracer.get_spans().is_empty());
970 }
971
972 #[test]
973 fn test_tracer_start_span() {
974 let tracer = SzTracer::new("test-service");
975 let span = tracer.start_span("test-operation");
976 assert_eq!(span.operation_name, "test-operation");
977 }
978
979 #[test]
980 fn test_tracer_end_span() {
981 let tracer = SzTracer::new("test-service");
982 let span = tracer.start_span("test-operation");
983 tracer.end_span(span);
984
985 let spans = tracer.get_spans();
986 assert_eq!(spans.len(), 1);
987 }
988
989 #[test]
990 fn test_tracer_inject() {
991 let tracer = SzTracer::new("test-service");
992 let span = tracer.start_span("test");
993 let headers = tracer.inject(&span);
994
995 let tp = headers
997 .get("traceparent")
998 .expect("traceparent header must be present");
999 let parts: Vec<&str> = tp.split('-').collect();
1001 assert_eq!(parts.len(), 4);
1002 assert_eq!(parts[0], "00"); assert_eq!(parts[1], span.trace_id);
1004 assert_eq!(parts[2], span.span_id);
1005 assert_eq!(parts[3], "01"); }
1007
1008 #[test]
1009 fn test_tracer_extract() {
1010 let tracer = SzTracer::new("test-service");
1011 let mut headers = HashMap::new();
1012 let trace_id = "0af7651916cd43dd8448eb211c80319c";
1014 let span_id = "b7ad6b7169203331";
1015 headers.insert(
1016 "traceparent".to_string(),
1017 format!("00-{}-{}-01", trace_id, span_id),
1018 );
1019
1020 let span = tracer.extract(&headers);
1021 assert!(span.is_some());
1022 let span = span.unwrap();
1023 assert_eq!(span.trace_id, trace_id);
1024 assert_eq!(span.span_id, span_id);
1025 }
1026
1027 #[test]
1028 fn test_tracer_extract_legacy_headers() {
1029 let tracer = SzTracer::new("test-service");
1031 let mut headers = HashMap::new();
1032 headers.insert("trace-id".to_string(), "trace123".to_string());
1033 headers.insert("span-id".to_string(), "span456".to_string());
1034
1035 let span = tracer.extract(&headers);
1036 assert!(span.is_some());
1037 let span = span.unwrap();
1038 assert_eq!(span.trace_id, "trace123");
1039 assert_eq!(span.span_id, "span456");
1040 }
1041
1042 #[test]
1043 fn test_tracer_extract_missing_headers() {
1044 let tracer = SzTracer::new("test-service");
1045 let headers = HashMap::new();
1046 let span = tracer.extract(&headers);
1047 assert!(span.is_none());
1048 }
1049
1050 #[test]
1053 fn test_parse_traceparent_valid() {
1054 let valid = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
1055 let span = SzTracer::parse_traceparent(valid).expect("valid traceparent must parse");
1056 assert_eq!(span.trace_id, "0af7651916cd43dd8448eb211c80319c");
1057 assert_eq!(span.span_id, "b7ad6b7169203331");
1058 }
1059
1060 #[test]
1061 fn test_parse_traceparent_invalid_version() {
1062 assert!(SzTracer::parse_traceparent(
1064 "0-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
1065 )
1066 .is_none());
1067 assert!(SzTracer::parse_traceparent(
1069 "xy-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
1070 )
1071 .is_none());
1072 }
1073
1074 #[test]
1075 fn test_parse_traceparent_invalid_trace_id_length() {
1076 assert!(SzTracer::parse_traceparent("00-short-b7ad6b7169203331-01").is_none());
1078 }
1079
1080 #[test]
1081 fn test_parse_traceparent_invalid_span_id_length() {
1082 assert!(
1084 SzTracer::parse_traceparent("00-0af7651916cd43dd8448eb211c80319c-short-01").is_none()
1085 );
1086 }
1087
1088 #[test]
1089 fn test_parse_traceparent_all_zeros_trace_id_rejected() {
1090 let all_zero = "00-00000000000000000000000000000000-b7ad6b7169203331-01";
1092 assert!(SzTracer::parse_traceparent(all_zero).is_none());
1093 }
1094
1095 #[test]
1096 fn test_parse_traceparent_all_zeros_span_id_rejected() {
1097 let all_zero = "00-0af7651916cd43dd8448eb211c80319c-0000000000000000-01";
1099 assert!(SzTracer::parse_traceparent(all_zero).is_none());
1100 }
1101
1102 #[test]
1103 fn test_parse_traceparent_invalid_flags() {
1104 assert!(SzTracer::parse_traceparent(
1106 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-xyz"
1107 )
1108 .is_none());
1109 }
1110
1111 #[test]
1112 fn test_parse_traceparent_wrong_part_count() {
1113 assert!(SzTracer::parse_traceparent(
1115 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331"
1116 )
1117 .is_none());
1118 assert!(SzTracer::parse_traceparent(
1119 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01-extra"
1120 )
1121 .is_none());
1122 }
1123
1124 #[test]
1125 fn test_inject_legacy_preserves_old_format() {
1126 let tracer = SzTracer::new("svc");
1127 let span = tracer.start_span("op");
1128 let headers = tracer.inject_legacy(&span);
1129
1130 assert_eq!(headers.get("trace-id"), Some(&span.trace_id.to_string()));
1131 assert_eq!(headers.get("span-id"), Some(&span.span_id.to_string()));
1132 assert!(!headers.contains_key("parent-span-id"));
1133 }
1134
1135 #[test]
1136 fn test_extract_legacy_preserves_old_format() {
1137 let tracer = SzTracer::new("svc");
1138 let mut headers = HashMap::new();
1139 headers.insert("trace-id".to_string(), "abc".to_string());
1140 headers.insert("span-id".to_string(), "def".to_string());
1141
1142 let span = tracer.extract_legacy(&headers).expect("legacy extract");
1143 assert_eq!(span.trace_id, "abc");
1144 assert_eq!(span.span_id, "def");
1145 }
1146
1147 #[test]
1148 fn test_w3c_traceparent_roundtrip_preserves_ids() {
1149 let tracer = SzTracer::new("svc");
1151 let original = tracer.start_span("roundtrip");
1152 let headers = tracer.inject(&original);
1153
1154 let extracted = tracer.extract(&headers).expect("roundtrip extract");
1155 assert_eq!(extracted.trace_id(), original.trace_id());
1156 assert_eq!(extracted.span_id(), original.span_id());
1157 assert!(extracted.parent_id().is_none());
1158 }
1159
1160 #[test]
1161 fn test_w3c_prefers_traceparent_over_legacy() {
1162 let tracer = SzTracer::new("svc");
1164 let mut headers = HashMap::new();
1165 headers.insert(
1166 "traceparent".to_string(),
1167 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_string(),
1168 );
1169 headers.insert("trace-id".to_string(), "legacy-trace".to_string());
1170 headers.insert("span-id".to_string(), "legacy-span".to_string());
1171
1172 let span = tracer.extract(&headers).expect("extract");
1173 assert_eq!(span.trace_id, "0af7651916cd43dd8448eb211c80319c");
1174 assert_eq!(span.span_id, "b7ad6b7169203331");
1175 }
1176
1177 #[test]
1178 fn test_w3c_falls_back_to_legacy_on_invalid_traceparent() {
1179 let tracer = SzTracer::new("svc");
1181 let mut headers = HashMap::new();
1182 headers.insert("traceparent".to_string(), "invalid-format".to_string());
1183 headers.insert("trace-id".to_string(), "legacy-trace".to_string());
1184 headers.insert("span-id".to_string(), "legacy-span".to_string());
1185
1186 let span = tracer
1187 .extract(&headers)
1188 .expect("should fall back to legacy");
1189 assert_eq!(span.trace_id, "legacy-trace");
1190 assert_eq!(span.span_id, "legacy-span");
1191 }
1192
1193 #[test]
1194 fn test_otel_tracer() {
1195 let tracer = OtelTracer::new("test-service");
1196 let span = tracer.start_span("test-operation");
1197 assert_eq!(span.operation_name, "test-operation");
1198 }
1199
1200 #[test]
1201 fn test_otel_tracer_is_a_sz_tracer_wrapper_not_a_real_otel_sdk() {
1202 let tracer = OtelTracer::new("svc");
1206 assert!(tracer.inner().get_spans().is_empty());
1207
1208 let span = tracer.start_span("op");
1209 assert_eq!(span.service_name(), "svc");
1210 tracer.end_span(span);
1211
1212 let spans = tracer.inner().get_spans();
1213 assert_eq!(spans.len(), 1);
1214 assert_eq!(spans[0].operation_name(), "op");
1215 assert!(spans[0].end_time.is_some());
1216 }
1217
1218 #[test]
1219 fn test_otel_tracer_inject_extract_roundtrip_preserves_ids() {
1220 let tracer = OtelTracer::new("svc");
1222 let original = tracer.start_span("roundtrip");
1223 let headers = tracer.inject(&original);
1224
1225 let tp = headers
1227 .get("traceparent")
1228 .expect("traceparent must be present");
1229 assert!(tp.contains(original.trace_id()));
1230 assert!(tp.contains(original.span_id()));
1231 assert!(!headers.contains_key("parent-span-id"));
1232
1233 let extracted = tracer.extract(&headers).expect("extract should round-trip");
1234 assert_eq!(extracted.trace_id(), original.trace_id());
1235 assert_eq!(extracted.span_id(), original.span_id());
1236 assert!(extracted.parent_id().is_none());
1237 }
1238
1239 #[test]
1240 fn test_otel_tracer_preserves_parent_id_through_roundtrip() {
1241 let tracer = OtelTracer::new("svc");
1242 let parent = tracer.start_span("parent");
1243 let child = tracer
1244 .start_span("child")
1245 .with_parent(parent.span_id().to_string());
1246 let headers = tracer.inject(&child);
1247
1248 assert_eq!(
1250 headers.get("parent-span-id"),
1251 Some(&parent.span_id().to_string())
1252 );
1253
1254 let extracted = tracer.extract(&headers).expect("extract should round-trip");
1255 assert_eq!(extracted.parent_id(), Some(parent.span_id()));
1256 }
1257
1258 #[test]
1259 fn test_otel_tracer_extract_returns_none_without_required_headers() {
1260 let tracer = OtelTracer::new("svc");
1261 let headers: HashMap<String, String> = HashMap::new();
1262 assert!(tracer.extract(&headers).is_none());
1263
1264 let mut partial = HashMap::new();
1266 partial.insert("trace-id".to_string(), "abc".to_string());
1267 assert!(tracer.extract(&partial).is_none());
1269
1270 let mut bad_w3c = HashMap::new();
1272 bad_w3c.insert("traceparent".to_string(), "garbage".to_string());
1273 assert!(tracer.extract(&bad_w3c).is_none());
1274 }
1275
1276 #[test]
1277 fn test_otel_tracer_generated_ids_have_correct_length() {
1278 let trace_id = SzTracer::generate_trace_id();
1283 let span_id = SzTracer::generate_span_id();
1284 assert_eq!(trace_id.len(), 32, "trace_id must be 32 hex chars");
1285 assert_eq!(span_id.len(), 16, "span_id must be 16 hex chars");
1286
1287 assert!(trace_id.chars().all(|c| c.is_ascii_hexdigit()));
1289 assert!(span_id.chars().all(|c| c.is_ascii_hexdigit()));
1290 }
1291
1292 #[test]
1293 fn test_span_accessors() {
1294 let span = Span::new("trace1", "span1", "test-op")
1295 .with_service("svc")
1296 .with_tag("k", "v");
1297
1298 assert_eq!(span.trace_id(), "trace1");
1299 assert_eq!(span.span_id(), "span1");
1300 assert_eq!(span.operation_name(), "test-op");
1301 assert_eq!(span.service_name(), "svc");
1302 assert_eq!(span.tags().get("k"), Some(&"v".to_string()));
1303 }
1304
1305 #[test]
1306 fn test_generate_ids() {
1307 let trace_id = SzTracer::generate_trace_id();
1308 let span_id = SzTracer::generate_span_id();
1309
1310 assert_eq!(trace_id.len(), 32);
1311 assert_eq!(span_id.len(), 16);
1312 }
1313
1314 #[test]
1315 fn test_tracer_clear() {
1316 let tracer = SzTracer::new("test-service");
1317
1318 let span = tracer.start_span("op1");
1319 tracer.end_span(span);
1320 let span = tracer.start_span("op2");
1321 tracer.end_span(span);
1322
1323 assert_eq!(tracer.get_spans().len(), 2);
1324
1325 tracer.clear();
1326 assert!(tracer.get_spans().is_empty());
1327 }
1328
1329 #[test]
1332 fn test_latency_histogram_new_empty() {
1333 let hist = LatencyHistogram::new(vec![
1334 Duration::from_millis(10),
1335 Duration::from_millis(100),
1336 Duration::from_millis(1000),
1337 ]);
1338 assert_eq!(hist.count(), 0);
1339 assert!(hist.percentile(50.0).is_none());
1340 assert!(hist.mean().is_none());
1341 }
1342
1343 #[test]
1344 fn test_latency_histogram_record_single() {
1345 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1346 hist.record(Duration::from_millis(50));
1347 assert_eq!(hist.count(), 1);
1348 assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(50)));
1349 assert_eq!(hist.mean(), Some(Duration::from_millis(50)));
1350 }
1351
1352 #[test]
1353 fn test_latency_histogram_percentile_p50_sorted() {
1354 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1355 for ms in [10, 20, 30, 40, 50] {
1356 hist.record(Duration::from_millis(ms));
1357 }
1358 assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(30)));
1360 }
1361
1362 #[test]
1363 fn test_latency_histogram_percentile_p95_high_value() {
1364 let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1365 for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1366 hist.record(Duration::from_millis(ms));
1367 }
1368 let p95 = hist.percentile(95.0).expect("p95 must exist");
1370 assert!(p95 >= Duration::from_millis(9));
1371 }
1372
1373 #[test]
1374 fn test_latency_histogram_percentile_p99_max_value() {
1375 let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1376 for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1377 hist.record(Duration::from_millis(ms));
1378 }
1379 let p99 = hist.percentile(99.0).expect("p99 must exist");
1380 assert_eq!(p99, Duration::from_millis(100));
1381 }
1382
1383 #[test]
1384 fn test_latency_histogram_percentile_p0_min_value() {
1385 let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1386 for ms in [10, 20, 30] {
1387 hist.record(Duration::from_millis(ms));
1388 }
1389 assert_eq!(hist.percentile(0.0), Some(Duration::from_millis(10)));
1390 }
1391
1392 #[test]
1393 fn test_latency_histogram_percentile_p100_max_value() {
1394 let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1395 for ms in [10, 20, 30] {
1396 hist.record(Duration::from_millis(ms));
1397 }
1398 assert_eq!(hist.percentile(100.0), Some(Duration::from_millis(30)));
1399 }
1400
1401 #[test]
1402 fn test_latency_histogram_percentile_empty_returns_none() {
1403 let hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1404 assert!(hist.percentile(50.0).is_none());
1405 }
1406
1407 #[test]
1408 fn test_latency_histogram_percentile_out_of_range_returns_none() {
1409 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1410 hist.record(Duration::from_millis(50));
1411 assert!(hist.percentile(-1.0).is_none());
1412 assert!(hist.percentile(101.0).is_none());
1413 }
1414
1415 #[test]
1416 fn test_latency_histogram_count() {
1417 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1418 assert_eq!(hist.count(), 0);
1419 hist.record(Duration::from_millis(10));
1420 hist.record(Duration::from_millis(20));
1421 hist.record(Duration::from_millis(30));
1422 assert_eq!(hist.count(), 3);
1423 }
1424
1425 #[test]
1426 fn test_latency_histogram_mean_multiple() {
1427 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1428 for ms in [10, 20, 30, 40, 50] {
1429 hist.record(Duration::from_millis(ms));
1430 }
1431 assert_eq!(hist.mean(), Some(Duration::from_millis(30)));
1433 }
1434
1435 #[test]
1436 fn test_latency_histogram_record_unsorted_input_stays_sorted() {
1437 let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1438 hist.record(Duration::from_millis(50));
1439 hist.record(Duration::from_millis(10));
1440 hist.record(Duration::from_millis(30));
1441 assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(30)));
1443 }
1444
1445 #[test]
1448 fn test_error_rate_counter_new_empty() {
1449 let counter = ErrorRateCounter::new(Duration::from_secs(60));
1450 assert_eq!(counter.total(), 0);
1451 assert_eq!(counter.errors(), 0);
1452 assert_eq!(counter.rate(), 0.0);
1453 }
1454
1455 #[test]
1456 fn test_error_rate_counter_all_success_rate_zero() {
1457 let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1458 for _ in 0..10 {
1459 counter.record(true);
1460 }
1461 assert_eq!(counter.total(), 10);
1462 assert_eq!(counter.errors(), 0);
1463 assert_eq!(counter.rate(), 0.0);
1464 }
1465
1466 #[test]
1467 fn test_error_rate_counter_all_failures_rate_one() {
1468 let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1469 for _ in 0..10 {
1470 counter.record(false);
1471 }
1472 assert_eq!(counter.total(), 10);
1473 assert_eq!(counter.errors(), 10);
1474 assert_eq!(counter.rate(), 1.0);
1475 }
1476
1477 #[test]
1478 fn test_error_rate_counter_mixed_rate() {
1479 let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1480 for _ in 0..7 {
1482 counter.record(true);
1483 }
1484 for _ in 0..3 {
1485 counter.record(false);
1486 }
1487 assert_eq!(counter.total(), 10);
1488 assert_eq!(counter.errors(), 3);
1489 let rate = counter.rate();
1490 assert!((rate - 0.3).abs() < 1e-9, "expected 0.3, got {rate}");
1491 }
1492
1493 #[test]
1494 fn test_error_rate_counter_empty_rate_is_zero() {
1495 let counter = ErrorRateCounter::new(Duration::from_secs(60));
1496 assert_eq!(counter.rate(), 0.0);
1498 }
1499
1500 #[test]
1501 fn test_error_rate_counter_window_expires_old_samples() {
1502 let mut counter = ErrorRateCounter::new(Duration::from_millis(100));
1504 counter.record(false);
1505 counter.record(false);
1506 std::thread::sleep(Duration::from_millis(120));
1508 counter.record(true);
1509 assert_eq!(counter.total(), 1);
1511 assert_eq!(counter.errors(), 0);
1512 assert_eq!(counter.rate(), 0.0);
1513 }
1514
1515 #[test]
1518 fn test_error_budget_new_is_full() {
1519 let budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1520 assert!((budget.remaining() - 1.0).abs() < 1e-9);
1521 assert!(!budget.is_exhausted());
1522 }
1523
1524 #[test]
1525 fn test_error_budget_consume_reduces_remaining() {
1526 let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1527 budget.consume(1);
1529 assert!((budget.remaining() - 0.999).abs() < 1e-9);
1531 assert!(!budget.is_exhausted());
1532 }
1533
1534 #[test]
1535 fn test_error_budget_exhausted_at_capacity() {
1536 let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1537 budget.consume(1000);
1539 assert_eq!(budget.remaining(), 0.0);
1540 assert!(budget.is_exhausted());
1541 }
1542
1543 #[test]
1544 fn test_error_budget_over_consume_clamps_to_zero() {
1545 let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1546 budget.consume(2000);
1547 assert_eq!(budget.remaining(), 0.0);
1548 assert!(budget.is_exhausted());
1549 }
1550
1551 #[test]
1552 fn test_error_budget_zero_errors_returns_one() {
1553 let budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1554 assert_eq!(budget.remaining(), 1.0);
1555 assert!(!budget.is_exhausted());
1556 }
1557
1558 #[test]
1559 fn test_error_budget_window_refills_after_expiry() {
1560 let mut budget = ErrorBudget::new(0.5, Duration::from_millis(100));
1562 budget.consume(2);
1563 assert!(budget.is_exhausted());
1564 std::thread::sleep(Duration::from_millis(120));
1566 assert!((budget.remaining() - 1.0).abs() < 1e-9);
1567 assert!(!budget.is_exhausted());
1568 }
1569
1570 #[test]
1571 fn test_error_budget_slo_one_means_no_errors_allowed() {
1572 let mut budget = ErrorBudget::new(1.0, Duration::from_secs(60));
1574 assert_eq!(budget.remaining(), 1.0);
1575 budget.consume(1);
1576 assert_eq!(budget.remaining(), 0.0);
1577 assert!(budget.is_exhausted());
1578 }
1579
1580 #[test]
1581 fn test_error_budget_multiple_consumes_accumulate() {
1582 let mut budget = ErrorBudget::new(0.99, Duration::from_secs(60));
1583 budget.consume(30);
1585 assert!((budget.remaining() - 0.7).abs() < 1e-9);
1586 budget.consume(30);
1587 assert!((budget.remaining() - 0.4).abs() < 1e-9);
1588 budget.consume(40);
1589 assert_eq!(budget.remaining(), 0.0);
1590 assert!(budget.is_exhausted());
1591 }
1592
1593 #[test]
1596 fn test_alert_level_variants_exist() {
1597 let info = AlertLevel::Info;
1598 let warning = AlertLevel::Warning;
1599 let critical = AlertLevel::Critical;
1600 assert_ne!(format!("{info:?}"), format!("{warning:?}"));
1602 assert_ne!(format!("{warning:?}"), format!("{critical:?}"));
1603 assert_ne!(format!("{info:?}"), format!("{critical:?}"));
1604 }
1605
1606 #[test]
1607 fn test_alert_construction_with_all_fields() {
1608 let ts = Utc::now();
1609 let alert = Alert {
1610 level: AlertLevel::Critical,
1611 message: "p99 latency exceeded budget".to_string(),
1612 timestamp: ts,
1613 operation: Some("query_user".to_string()),
1614 };
1615 assert_eq!(alert.level, AlertLevel::Critical);
1616 assert_eq!(alert.message, "p99 latency exceeded budget");
1617 assert_eq!(alert.timestamp, ts);
1618 assert_eq!(alert.operation.as_deref(), Some("query_user"));
1619 }
1620
1621 #[test]
1622 fn test_alert_construction_without_operation() {
1623 let alert = Alert {
1624 level: AlertLevel::Info,
1625 message: "system healthy".to_string(),
1626 timestamp: Utc::now(),
1627 operation: None,
1628 };
1629 assert!(alert.operation.is_none());
1630 }
1631
1632 #[test]
1633 fn test_alert_implements_clone_debug() {
1634 let alert = Alert {
1635 level: AlertLevel::Warning,
1636 message: "approaching budget".to_string(),
1637 timestamp: Utc::now(),
1638 operation: Some("op".to_string()),
1639 };
1640 let cloned = alert.clone();
1641 assert_eq!(cloned.level, alert.level);
1642 assert_eq!(cloned.message, alert.message);
1643 let debug_str = format!("{alert:?}");
1645 assert!(
1646 debug_str.contains("approaching budget"),
1647 "debug output missing message: {}",
1648 debug_str
1649 );
1650 assert!(
1651 debug_str.contains("Warning"),
1652 "debug output missing level: {}",
1653 debug_str
1654 );
1655 }
1656
1657 #[test]
1660 fn test_saturation_gauge_new_starts_unsaturated() {
1661 let gauge = SaturationGauge::new(0.8);
1662 assert!(!gauge.is_saturated());
1663 assert!(gauge.check_alert().is_none());
1664 }
1665
1666 #[test]
1667 fn test_saturation_gauge_set_below_threshold_not_saturated() {
1668 let mut gauge = SaturationGauge::new(0.8);
1669 gauge.set(0.5);
1670 assert!(!gauge.is_saturated());
1671 assert!(gauge.check_alert().is_none());
1672 }
1673
1674 #[test]
1675 fn test_saturation_gauge_set_at_threshold_is_saturated() {
1676 let mut gauge = SaturationGauge::new(0.8);
1677 gauge.set(0.8);
1678 assert!(gauge.is_saturated());
1679 }
1680
1681 #[test]
1682 fn test_saturation_gauge_set_above_threshold_is_saturated() {
1683 let mut gauge = SaturationGauge::new(0.8);
1684 gauge.set(0.95);
1685 assert!(gauge.is_saturated());
1686 }
1687
1688 #[test]
1689 fn test_saturation_gauge_check_alert_returns_critical_when_saturated() {
1690 let mut gauge = SaturationGauge::new(0.8);
1691 gauge.set(0.95);
1692 let alert = gauge.check_alert().expect("alert must fire when saturated");
1693 assert_eq!(alert.level, AlertLevel::Critical);
1694 assert!(!alert.message.is_empty());
1695 assert!(alert.operation.is_none()); }
1697
1698 #[test]
1699 fn test_saturation_gauge_check_alert_returns_none_when_not_saturated() {
1700 let mut gauge = SaturationGauge::new(0.8);
1701 gauge.set(0.3);
1702 assert!(gauge.check_alert().is_none());
1703 }
1704
1705 #[test]
1706 fn test_saturation_gauge_set_zero_not_saturated() {
1707 let mut gauge = SaturationGauge::new(0.8);
1708 gauge.set(0.0);
1709 assert!(!gauge.is_saturated());
1710 }
1711
1712 #[test]
1713 fn test_saturation_gauge_set_one_saturated() {
1714 let mut gauge = SaturationGauge::new(0.5);
1715 gauge.set(1.0);
1716 assert!(gauge.is_saturated());
1717 }
1718
1719 #[test]
1720 fn test_saturation_gauge_threshold_zero_always_saturated() {
1721 let mut gauge = SaturationGauge::new(0.0);
1722 gauge.set(0.0);
1723 assert!(gauge.is_saturated());
1726 }
1727
1728 fn sample_alert(level: AlertLevel, op: Option<&str>) -> Alert {
1731 Alert {
1732 level,
1733 message: "test alert".to_string(),
1734 timestamp: Utc::now(),
1735 operation: op.map(str::to_string),
1736 }
1737 }
1738
1739 #[test]
1740 fn test_log_alert_hook_notify_returns_ok() {
1741 let hook = LogAlertHook::new();
1742 let alert = sample_alert(AlertLevel::Warning, Some("op"));
1743 let result = hook.notify(&alert);
1744 assert!(result.is_ok());
1745 }
1746
1747 #[test]
1748 fn test_log_alert_hook_notify_critical_succeeds() {
1749 let hook = LogAlertHook::new();
1750 let alert = sample_alert(AlertLevel::Critical, None);
1751 let result = hook.notify(&alert);
1752 assert!(result.is_ok());
1753 }
1754
1755 #[test]
1756 fn test_log_alert_hook_implements_send_sync() {
1757 fn assert_send_sync<T: Send + Sync>() {}
1758 assert_send_sync::<LogAlertHook>();
1759 }
1760
1761 #[test]
1762 fn test_webhook_alert_hook_new_starts_empty() {
1763 let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1764 assert!(hook.sent_alerts().is_empty());
1765 }
1766
1767 #[test]
1768 fn test_webhook_alert_hook_notify_stores_alert() {
1769 let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1770 let alert = sample_alert(AlertLevel::Critical, Some("query_user"));
1771 hook.notify(&alert).expect("notify must succeed");
1772 let sent = hook.sent_alerts();
1773 assert_eq!(sent.len(), 1);
1774 assert_eq!(sent[0], alert);
1775 }
1776
1777 #[test]
1778 fn test_webhook_alert_hook_multiple_notifications_accumulate() {
1779 let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1780 let a1 = sample_alert(AlertLevel::Info, None);
1781 let a2 = sample_alert(AlertLevel::Warning, Some("op1"));
1782 let a3 = sample_alert(AlertLevel::Critical, Some("op2"));
1783 hook.notify(&a1).unwrap();
1784 hook.notify(&a2).unwrap();
1785 hook.notify(&a3).unwrap();
1786 let sent = hook.sent_alerts();
1787 assert_eq!(sent.len(), 3);
1788 assert_eq!(sent[0], a1);
1789 assert_eq!(sent[1], a2);
1790 assert_eq!(sent[2], a3);
1791 }
1792
1793 #[test]
1794 fn test_webhook_alert_hook_sent_alerts_returns_clone() {
1795 let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1797 let alert = sample_alert(AlertLevel::Info, None);
1798 hook.notify(&alert).unwrap();
1799 let mut sent = hook.sent_alerts();
1800 sent.clear();
1801 assert_eq!(hook.sent_alerts().len(), 1);
1803 }
1804
1805 #[test]
1806 fn test_webhook_alert_hook_implements_send_sync() {
1807 fn assert_send_sync<T: Send + Sync>() {}
1808 assert_send_sync::<InMemoryAlertHook>();
1809 }
1810
1811 #[test]
1812 fn test_alert_hook_trait_object_dispatch() {
1813 let hooks: Vec<Box<dyn AlertHook>> = vec![
1815 Box::new(LogAlertHook::new()),
1816 Box::new(InMemoryAlertHook::new(
1817 "https://example.com/hook".to_string(),
1818 )),
1819 ];
1820 let alert = sample_alert(AlertLevel::Critical, Some("op"));
1821 for hook in &hooks {
1822 assert!(hook.notify(&alert).is_ok());
1823 }
1824 let webhook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1827 webhook.notify(&alert).unwrap();
1828 assert_eq!(webhook.sent_alerts().len(), 1);
1829 }
1830
1831 #[test]
1834 fn test_sla_monitor_new_empty() {
1835 let monitor = SlaMonitor::new(0.999);
1836 assert!(monitor.operations().is_empty());
1837 }
1838
1839 #[test]
1840 fn test_sla_monitor_observe_creates_operation() {
1841 let monitor = SlaMonitor::new(0.999);
1842 monitor.observe("query", Duration::from_millis(50), true);
1843 let ops = monitor.operations();
1844 assert_eq!(ops, vec!["query".to_string()]);
1845 }
1846
1847 #[test]
1848 fn test_sla_monitor_report_unknown_returns_none() {
1849 let monitor = SlaMonitor::new(0.999);
1850 assert!(monitor.report("unknown").is_none());
1851 }
1852
1853 #[test]
1854 fn test_sla_monitor_report_basic_stats_all_success() {
1855 let monitor = SlaMonitor::new(0.999);
1856 for ms in [10, 20, 30, 40, 50] {
1857 monitor.observe("op", Duration::from_millis(ms), true);
1858 }
1859 let report = monitor.report("op").expect("report must exist");
1860 assert_eq!(report.total_count, 5);
1861 assert_eq!(report.error_rate, 0.0);
1862 assert!((report.slo_target - 0.999).abs() < 1e-9);
1863 assert!((report.p50_ms - 30.0).abs() < 1e-9);
1865 assert!((report.error_budget_remaining - 1.0).abs() < 1e-9);
1867 assert!((report.saturation - 0.0).abs() < 1e-9);
1868 }
1869
1870 #[test]
1871 fn test_sla_monitor_report_with_errors_over_budget() {
1872 let monitor = SlaMonitor::new(0.999);
1873 for _ in 0..8 {
1875 monitor.observe("op", Duration::from_millis(10), true);
1876 }
1877 for _ in 0..2 {
1878 monitor.observe("op", Duration::from_millis(10), false);
1879 }
1880 let report = monitor.report("op").expect("report must exist");
1881 assert_eq!(report.total_count, 10);
1882 assert!((report.error_rate - 0.2).abs() < 1e-9);
1883 assert!((report.saturation - 1.0).abs() < 1e-9);
1886 assert!((report.error_budget_remaining - 0.0).abs() < 1e-9);
1887 }
1888
1889 #[test]
1890 fn test_sla_monitor_report_partial_budget() {
1891 let monitor = SlaMonitor::new(0.9);
1896 for i in 0..20 {
1897 monitor.observe("op", Duration::from_millis(i), i != 5);
1898 }
1899 let report = monitor.report("op").expect("report");
1900 assert!((report.error_rate - 0.05).abs() < 1e-9);
1901 assert!((report.saturation - 0.5).abs() < 1e-9);
1902 assert!((report.error_budget_remaining - 0.5).abs() < 1e-9);
1903 }
1904
1905 #[test]
1906 fn test_sla_monitor_operations_isolated() {
1907 let monitor = SlaMonitor::new(0.999);
1908 monitor.observe("op1", Duration::from_millis(10), true);
1909 monitor.observe("op2", Duration::from_millis(20), false);
1910 let mut ops = monitor.operations();
1911 ops.sort();
1912 assert_eq!(ops, vec!["op1".to_string(), "op2".to_string()]);
1913
1914 let r1 = monitor.report("op1").expect("op1 report");
1915 let r2 = monitor.report("op2").expect("op2 report");
1916 assert_eq!(r1.total_count, 1);
1917 assert_eq!(r2.total_count, 1);
1918 assert!((r1.error_rate - 0.0).abs() < 1e-9);
1919 assert!((r2.error_rate - 1.0).abs() < 1e-9);
1920 }
1921
1922 #[test]
1923 fn test_sla_monitor_p95_p99_high_percentile() {
1924 let monitor = SlaMonitor::new(0.999);
1925 for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1926 monitor.observe("op", Duration::from_millis(ms), true);
1927 }
1928 let report = monitor.report("op").expect("report");
1929 assert!((report.p95_ms - 100.0).abs() < 1e-9);
1931 assert!((report.p99_ms - 100.0).abs() < 1e-9);
1932 }
1933
1934 #[test]
1935 fn test_sla_monitor_observe_aggregates_multiple_calls() {
1936 let monitor = SlaMonitor::new(0.999);
1937 for _ in 0..100 {
1938 monitor.observe("op", Duration::from_millis(5), true);
1939 }
1940 let report = monitor.report("op").expect("report");
1941 assert_eq!(report.total_count, 100);
1942 assert!((report.p50_ms - 5.0).abs() < 1e-9);
1943 }
1944
1945 #[test]
1946 fn test_sla_monitor_implements_send_sync() {
1947 fn assert_send_sync<T: Send + Sync>() {}
1948 assert_send_sync::<SlaMonitor>();
1949 }
1950
1951 #[test]
1952 fn test_sla_monitor_concurrent_observe_thread_safe() {
1953 use std::sync::Arc;
1954 use std::thread;
1955
1956 let monitor = Arc::new(SlaMonitor::new(0.999));
1957 let mut handles = vec![];
1958
1959 for t in 0..4 {
1960 let m = Arc::clone(&monitor);
1961 handles.push(thread::spawn(move || {
1962 for i in 0..100 {
1963 m.observe("op", Duration::from_millis(i as u64), i % 10 != 0);
1965 }
1966 let op_name = format!("thread-{t}");
1968 m.observe(&op_name, Duration::from_millis(1), true);
1969 }));
1970 }
1971
1972 for h in handles {
1973 h.join().unwrap();
1974 }
1975
1976 let report = monitor.report("op").expect("op report");
1977 assert_eq!(report.total_count, 400);
1978 assert!((report.error_rate - 0.1).abs() < 1e-9);
1980
1981 let mut ops = monitor.operations();
1983 ops.sort();
1984 assert_eq!(ops.len(), 5);
1986 assert!(ops.contains(&"op".to_string()));
1987 }
1988
1989 #[test]
1990 fn test_sla_monitor_report_slo_one_with_no_errors() {
1991 let monitor = SlaMonitor::new(1.0);
1993 monitor.observe("op", Duration::from_millis(10), true);
1994 let report = monitor.report("op").expect("report");
1995 assert!((report.error_budget_remaining - 1.0).abs() < 1e-9);
1996 assert!((report.saturation - 0.0).abs() < 1e-9);
1997 }
1998
1999 #[test]
2000 fn test_sla_monitor_report_slo_one_with_errors() {
2001 let monitor = SlaMonitor::new(1.0);
2003 monitor.observe("op", Duration::from_millis(10), false);
2004 let report = monitor.report("op").expect("report");
2005 assert!((report.error_budget_remaining - 0.0).abs() < 1e-9);
2006 assert!((report.saturation - 1.0).abs() < 1e-9);
2007 }
2008
2009 #[test]
2010 fn test_sla_report_fields_are_public() {
2011 let monitor = SlaMonitor::new(0.999);
2013 monitor.observe("op", Duration::from_millis(10), true);
2014 let r = monitor.report("op").unwrap();
2015 let _p50: f64 = r.p50_ms;
2016 let _p95: f64 = r.p95_ms;
2017 let _p99: f64 = r.p99_ms;
2018 let _erate: f64 = r.error_rate;
2019 let _total: usize = r.total_count;
2020 let _slo: f64 = r.slo_target;
2021 let _ebr: f64 = r.error_budget_remaining;
2022 let _sat: f64 = r.saturation;
2023 }
2024}
2025
2026#[cfg(feature = "otlp")]
2048#[derive(Debug, Clone)]
2049pub struct OtlpConfig {
2050 pub endpoint: String,
2052 pub service_name: String,
2054 pub timeout_ms: u64,
2056}
2057
2058#[cfg(feature = "otlp")]
2059impl Default for OtlpConfig {
2060 fn default() -> Self {
2061 Self {
2062 endpoint: "http://localhost:4317".to_string(),
2063 service_name: "sz-orm".to_string(),
2064 timeout_ms: 5000,
2065 }
2066 }
2067}
2068
2069#[cfg(feature = "otlp")]
2091pub async fn init_otlp_exporter(config: OtlpConfig) -> Result<OtlpGuard, TracingError> {
2092 use opentelemetry_otlp::{SpanExporter, WithExportConfig};
2093 use opentelemetry_sdk::resource::Resource;
2094 use opentelemetry_sdk::runtime::Tokio;
2095 use opentelemetry_sdk::trace::TracerProvider;
2096 use std::time::Duration;
2097
2098 let exporter = SpanExporter::builder()
2099 .with_tonic()
2100 .with_endpoint(config.endpoint.clone())
2101 .with_timeout(Duration::from_millis(config.timeout_ms))
2102 .build()
2103 .map_err(|e| TracingError::OtlpInitFailed(format!("exporter build: {e}")))?;
2104
2105 let provider = TracerProvider::builder()
2106 .with_batch_exporter(exporter, Tokio)
2107 .with_resource(Resource::new_with_defaults([opentelemetry::KeyValue::new(
2108 "service.name",
2109 config.service_name.clone(),
2110 )]))
2111 .build();
2112
2113 opentelemetry::global::set_tracer_provider(provider.clone());
2115
2116 Ok(OtlpGuard { provider })
2117}
2118
2119#[cfg(feature = "otlp")]
2123pub struct OtlpGuard {
2124 provider: opentelemetry_sdk::trace::TracerProvider,
2125}
2126
2127#[cfg(feature = "otlp")]
2128impl Drop for OtlpGuard {
2129 fn drop(&mut self) {
2130 let _ = self.provider.shutdown();
2132 }
2133}