1use std::fmt;
89use std::sync::atomic::{AtomicU64, Ordering};
90use std::time::Instant;
91
92use crate::context::RequestContext;
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
100pub enum LogLevel {
101 Trace = 0,
103 Debug = 1,
105 Info = 2,
107 Warn = 3,
109 Error = 4,
111}
112
113impl LogLevel {
114 #[must_use]
116 pub const fn as_str(&self) -> &'static str {
117 match self {
118 Self::Trace => "trace",
119 Self::Debug => "debug",
120 Self::Info => "info",
121 Self::Warn => "warn",
122 Self::Error => "error",
123 }
124 }
125
126 #[must_use]
128 pub const fn as_char(&self) -> char {
129 match self {
130 Self::Trace => 'T',
131 Self::Debug => 'D',
132 Self::Info => 'I',
133 Self::Warn => 'W',
134 Self::Error => 'E',
135 }
136 }
137}
138
139impl fmt::Display for LogLevel {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 write!(f, "{}", self.as_str())
142 }
143}
144
145#[derive(Debug)]
150pub struct LogEntry {
151 pub level: LogLevel,
153 pub message: String,
155 pub request_id: u64,
157 pub region_id: String,
159 pub task_id: String,
161 pub target: Option<String>,
163 pub fields: Vec<(String, String)>,
165 pub timestamp_ns: u64,
167}
168
169impl LogEntry {
170 #[must_use]
172 pub fn new(ctx: &RequestContext, level: LogLevel, message: impl Into<String>) -> Self {
173 Self {
174 level,
175 message: message.into(),
176 request_id: ctx.request_id(),
177 region_id: format!("{:?}", ctx.region_id()),
178 task_id: format!("{:?}", ctx.task_id()),
179 target: None,
180 fields: Vec::new(),
181 timestamp_ns: 0, }
183 }
184
185 #[must_use]
187 pub fn target(mut self, target: impl Into<String>) -> Self {
188 self.target = Some(target.into());
189 self
190 }
191
192 #[must_use]
196 pub fn field(mut self, key: impl Into<String>, value: impl fmt::Display) -> Self {
197 if self.fields.len() < 16 {
198 self.fields.push((key.into(), value.to_string()));
199 }
200 self
201 }
202
203 #[must_use]
205 pub fn to_json(&self) -> String {
206 let mut json = format!(
207 r#"{{"timestamp_ns":{},"level":"{}","message":"{}","request_id":{},"region_id":"{}","task_id":"{}""#,
208 self.timestamp_ns,
209 self.level,
210 escape_json(&self.message),
211 self.request_id,
212 escape_json(&self.region_id),
213 escape_json(&self.task_id)
214 );
215
216 if let Some(ref target) = self.target {
217 json.push_str(&format!(r#","target":"{}""#, escape_json(target)));
218 }
219
220 if !self.fields.is_empty() {
221 json.push_str(r#","fields":{"#);
222 for (i, (k, v)) in self.fields.iter().enumerate() {
223 if i > 0 {
224 json.push(',');
225 }
226 json.push_str(&format!(r#""{}":"{}""#, escape_json(k), escape_json(v)));
227 }
228 json.push('}');
229 }
230
231 json.push('}');
232 json
233 }
234
235 #[must_use]
237 pub fn to_compact(&self) -> String {
238 let mut output = format!(
239 "[{}] req={} {}",
240 self.level.as_char(),
241 self.request_id,
242 self.message
243 );
244
245 if !self.fields.is_empty() {
246 output.push_str(" {");
247 for (i, (k, v)) in self.fields.iter().enumerate() {
248 if i > 0 {
249 output.push_str(", ");
250 }
251 output.push_str(&format!("{k}={v}"));
252 }
253 output.push('}');
254 }
255
256 output
257 }
258}
259
260fn escape_json(s: &str) -> String {
262 s.replace('\\', "\\\\")
263 .replace('"', "\\\"")
264 .replace('\n', "\\n")
265 .replace('\r', "\\r")
266 .replace('\t', "\\t")
267}
268
269pub struct Span {
274 name: String,
275 request_id: u64,
276 start: Instant,
277 span_id: u64,
278 parent_id: Option<u64>,
279 ended: bool,
280}
281
282impl Span {
283 #[must_use]
285 pub fn new(ctx: &RequestContext, name: impl Into<String>) -> Self {
286 static SPAN_COUNTER: AtomicU64 = AtomicU64::new(1);
287
288 Self {
289 name: name.into(),
290 request_id: ctx.request_id(),
291 start: Instant::now(),
292 span_id: SPAN_COUNTER.fetch_add(1, Ordering::SeqCst),
293 parent_id: None,
294 ended: false,
295 }
296 }
297
298 #[must_use]
300 pub fn child(&self, ctx: &RequestContext, name: impl Into<String>) -> Self {
301 static SPAN_COUNTER: AtomicU64 = AtomicU64::new(1);
302
303 Self {
304 name: name.into(),
305 request_id: ctx.request_id(),
306 start: Instant::now(),
307 span_id: SPAN_COUNTER.fetch_add(1, Ordering::SeqCst),
308 parent_id: Some(self.span_id),
309 ended: false,
310 }
311 }
312
313 #[must_use]
315 pub fn name(&self) -> &str {
316 &self.name
317 }
318
319 #[must_use]
321 pub fn span_id(&self) -> u64 {
322 self.span_id
323 }
324
325 #[must_use]
327 pub fn parent_id(&self) -> Option<u64> {
328 self.parent_id
329 }
330
331 #[must_use]
333 pub fn elapsed(&self) -> std::time::Duration {
334 self.start.elapsed()
335 }
336
337 pub fn end(&mut self) -> std::time::Duration {
342 let duration = self.elapsed();
343 if !self.ended {
344 self.ended = true;
345 }
348 duration
349 }
350
351 #[must_use]
353 pub fn to_json(&self) -> String {
354 let duration = self.elapsed();
355 let mut json = format!(
356 r#"{{"span_id":{},"name":"{}","request_id":{},"duration_us":{}"#,
357 self.span_id,
358 escape_json(&self.name),
359 self.request_id,
360 duration.as_micros()
361 );
362
363 if let Some(parent) = self.parent_id {
364 json.push_str(&format!(r#","parent_id":{parent}"#));
365 }
366
367 json.push('}');
368 json
369 }
370}
371
372impl Drop for Span {
373 fn drop(&mut self) {
374 if !self.ended {
375 self.end();
376 }
377 }
378}
379
380pub struct AutoSpan {
385 inner: Span,
386 ctx_request_id: u64,
387}
388
389impl AutoSpan {
390 #[must_use]
392 pub fn new(ctx: &RequestContext, name: impl Into<String>) -> Self {
393 Self {
394 inner: Span::new(ctx, name),
395 ctx_request_id: ctx.request_id(),
396 }
397 }
398}
399
400impl Drop for AutoSpan {
401 fn drop(&mut self) {
402 let duration = self.inner.end();
403 let _ = (duration, self.ctx_request_id); }
407}
408
409#[derive(Debug, Clone)]
411pub struct LogConfig {
412 pub min_level: LogLevel,
414 pub json_output: bool,
416 pub include_target: bool,
418 pub max_fields: usize,
420}
421
422impl Default for LogConfig {
423 fn default() -> Self {
424 Self {
425 min_level: LogLevel::Info,
426 json_output: true,
427 include_target: true,
428 max_fields: 16,
429 }
430 }
431}
432
433impl LogConfig {
434 #[must_use]
436 pub fn new() -> Self {
437 Self::default()
438 }
439
440 #[must_use]
442 pub fn level(mut self, level: LogLevel) -> Self {
443 self.min_level = level;
444 self
445 }
446
447 #[must_use]
449 pub fn json_output(mut self, json: bool) -> Self {
450 self.json_output = json;
451 self
452 }
453
454 #[must_use]
456 pub fn include_target(mut self, include: bool) -> Self {
457 self.include_target = include;
458 self
459 }
460
461 #[must_use]
463 pub fn max_fields(mut self, max: usize) -> Self {
464 self.max_fields = max;
465 self
466 }
467
468 #[must_use]
470 pub fn development() -> Self {
471 Self {
472 min_level: LogLevel::Debug,
473 json_output: false,
474 include_target: true,
475 max_fields: 16,
476 }
477 }
478
479 #[must_use]
481 pub fn production() -> Self {
482 Self {
483 min_level: LogLevel::Info,
484 json_output: true,
485 include_target: true,
486 max_fields: 16,
487 }
488 }
489
490 #[must_use]
492 pub fn testing() -> Self {
493 Self {
494 min_level: LogLevel::Trace,
495 json_output: true,
496 include_target: true,
497 max_fields: 16,
498 }
499 }
500}
501
502use std::sync::atomic::AtomicUsize;
507
508static GLOBAL_LOG_LEVEL: AtomicUsize = AtomicUsize::new(LogLevel::Info as usize);
513
514#[inline]
516#[must_use]
517pub fn global_log_level() -> LogLevel {
518 match GLOBAL_LOG_LEVEL.load(Ordering::Relaxed) {
519 0 => LogLevel::Trace,
520 1 => LogLevel::Debug,
521 2 => LogLevel::Info,
522 3 => LogLevel::Warn,
523 _ => LogLevel::Error,
524 }
525}
526
527pub fn set_global_log_level(level: LogLevel) {
531 GLOBAL_LOG_LEVEL.store(level as usize, Ordering::Relaxed);
532}
533
534#[inline]
536#[must_use]
537pub fn level_enabled(level: LogLevel) -> bool {
538 level >= global_log_level()
539}
540
541pub struct RequestLogger<'a> {
561 ctx: &'a RequestContext,
562 config: LogConfig,
563}
564
565impl<'a> RequestLogger<'a> {
566 #[must_use]
568 pub fn new(ctx: &'a RequestContext, config: LogConfig) -> Self {
569 Self { ctx, config }
570 }
571
572 #[inline]
574 #[must_use]
575 pub fn is_enabled(&self, level: LogLevel) -> bool {
576 level >= self.config.min_level && level_enabled(level)
577 }
578
579 pub fn emit(&self, entry: LogEntry) {
581 if !self.is_enabled(entry.level) {
582 return;
583 }
584
585 let output = if self.config.json_output {
586 entry.to_json()
587 } else {
588 entry.to_compact()
589 };
590
591 eprintln!("{output}");
594 }
595
596 pub fn trace(&self, message: impl Into<String>) {
598 if self.is_enabled(LogLevel::Trace) {
599 self.emit(LogEntry::new(self.ctx, LogLevel::Trace, message));
600 }
601 }
602
603 pub fn debug(&self, message: impl Into<String>) {
605 if self.is_enabled(LogLevel::Debug) {
606 self.emit(LogEntry::new(self.ctx, LogLevel::Debug, message));
607 }
608 }
609
610 pub fn info(&self, message: impl Into<String>) {
612 if self.is_enabled(LogLevel::Info) {
613 self.emit(LogEntry::new(self.ctx, LogLevel::Info, message));
614 }
615 }
616
617 pub fn warn(&self, message: impl Into<String>) {
619 if self.is_enabled(LogLevel::Warn) {
620 self.emit(LogEntry::new(self.ctx, LogLevel::Warn, message));
621 }
622 }
623
624 pub fn error(&self, message: impl Into<String>) {
626 if self.is_enabled(LogLevel::Error) {
627 self.emit(LogEntry::new(self.ctx, LogLevel::Error, message));
628 }
629 }
630
631 pub fn trace_with_fields<F>(&self, message: impl Into<String>, f: F)
633 where
634 F: FnOnce(LogEntry) -> LogEntry,
635 {
636 if self.is_enabled(LogLevel::Trace) {
637 let entry = f(LogEntry::new(self.ctx, LogLevel::Trace, message));
638 self.emit(entry);
639 }
640 }
641
642 pub fn debug_with_fields<F>(&self, message: impl Into<String>, f: F)
644 where
645 F: FnOnce(LogEntry) -> LogEntry,
646 {
647 if self.is_enabled(LogLevel::Debug) {
648 let entry = f(LogEntry::new(self.ctx, LogLevel::Debug, message));
649 self.emit(entry);
650 }
651 }
652
653 pub fn info_with_fields<F>(&self, message: impl Into<String>, f: F)
655 where
656 F: FnOnce(LogEntry) -> LogEntry,
657 {
658 if self.is_enabled(LogLevel::Info) {
659 let entry = f(LogEntry::new(self.ctx, LogLevel::Info, message));
660 self.emit(entry);
661 }
662 }
663
664 pub fn warn_with_fields<F>(&self, message: impl Into<String>, f: F)
666 where
667 F: FnOnce(LogEntry) -> LogEntry,
668 {
669 if self.is_enabled(LogLevel::Warn) {
670 let entry = f(LogEntry::new(self.ctx, LogLevel::Warn, message));
671 self.emit(entry);
672 }
673 }
674
675 pub fn error_with_fields<F>(&self, message: impl Into<String>, f: F)
677 where
678 F: FnOnce(LogEntry) -> LogEntry,
679 {
680 if self.is_enabled(LogLevel::Error) {
681 let entry = f(LogEntry::new(self.ctx, LogLevel::Error, message));
682 self.emit(entry);
683 }
684 }
685
686 #[must_use]
688 pub fn span(&self, name: impl Into<String>) -> Span {
689 Span::new(self.ctx, name)
690 }
691
692 #[must_use]
694 pub fn span_auto(&self, name: impl Into<String>) -> AutoSpan {
695 AutoSpan::new(self.ctx, name)
696 }
697}
698
699#[macro_export]
716macro_rules! log_trace {
717 ($ctx:expr, $msg:expr) => {
718 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Trace, $msg)
719 .target(module_path!())
720 };
721 ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
722 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Trace, $msg)
723 .target(module_path!())
724 $(.field(stringify!($key), $value))+
725 };
726 ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
727 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Trace, format!($fmt, $($arg)*))
728 .target(module_path!())
729 };
730}
731
732#[macro_export]
734macro_rules! log_debug {
735 ($ctx:expr, $msg:expr) => {
736 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Debug, $msg)
737 .target(module_path!())
738 };
739 ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
740 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Debug, $msg)
741 .target(module_path!())
742 $(.field(stringify!($key), $value))+
743 };
744 ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
745 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Debug, format!($fmt, $($arg)*))
746 .target(module_path!())
747 };
748}
749
750#[macro_export]
752macro_rules! log_info {
753 ($ctx:expr, $msg:expr) => {
754 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Info, $msg)
755 .target(module_path!())
756 };
757 ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
758 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Info, $msg)
759 .target(module_path!())
760 $(.field(stringify!($key), $value))+
761 };
762 ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
763 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Info, format!($fmt, $($arg)*))
764 .target(module_path!())
765 };
766}
767
768#[macro_export]
770macro_rules! log_warn {
771 ($ctx:expr, $msg:expr) => {
772 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Warn, $msg)
773 .target(module_path!())
774 };
775 ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
776 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Warn, $msg)
777 .target(module_path!())
778 $(.field(stringify!($key), $value))+
779 };
780 ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
781 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Warn, format!($fmt, $($arg)*))
782 .target(module_path!())
783 };
784}
785
786#[macro_export]
788macro_rules! log_error {
789 ($ctx:expr, $msg:expr) => {
790 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Error, $msg)
791 .target(module_path!())
792 };
793 ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
794 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Error, $msg)
795 .target(module_path!())
796 $(.field(stringify!($key), $value))+
797 };
798 ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
799 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Error, format!($fmt, $($arg)*))
800 .target(module_path!())
801 };
802}
803
804#[cfg(test)]
805mod tests {
806 use super::*;
807 use asupersync::Cx;
808
809 fn test_context() -> crate::context::RequestContext {
810 let cx = Cx::for_testing();
811 crate::context::RequestContext::new(cx, 12345)
812 }
813
814 #[test]
815 fn log_level_ordering() {
816 assert!(LogLevel::Trace < LogLevel::Debug);
817 assert!(LogLevel::Debug < LogLevel::Info);
818 assert!(LogLevel::Info < LogLevel::Warn);
819 assert!(LogLevel::Warn < LogLevel::Error);
820 }
821
822 #[test]
823 fn log_level_display() {
824 assert_eq!(LogLevel::Info.as_str(), "info");
825 assert_eq!(LogLevel::Error.as_char(), 'E');
826 }
827
828 #[test]
829 fn log_entry_json() {
830 let ctx = test_context();
831 let entry = LogEntry::new(&ctx, LogLevel::Info, "Test message")
832 .target("test::module")
833 .field("user_id", 42)
834 .field("action", "login");
835
836 let json = entry.to_json();
837 assert!(json.contains(r#""level":"info""#));
838 assert!(json.contains(r#""message":"Test message""#));
839 assert!(json.contains(r#""request_id":12345"#));
840 assert!(json.contains(r#""target":"test::module""#));
841 assert!(json.contains(r#""user_id":"42""#));
842 assert!(json.contains(r#""action":"login""#));
843 }
844
845 #[test]
846 fn log_entry_compact() {
847 let ctx = test_context();
848 let entry =
849 LogEntry::new(&ctx, LogLevel::Warn, "Something happened").field("error_code", "E001");
850
851 let compact = entry.to_compact();
852 assert!(compact.starts_with("[W] req=12345"));
853 assert!(compact.contains("Something happened"));
854 assert!(compact.contains("error_code=E001"));
855 }
856
857 #[test]
858 fn span_timing() {
859 let ctx = test_context();
860 let mut span = Span::new(&ctx, "test_operation");
861
862 std::thread::sleep(std::time::Duration::from_millis(1));
863 let duration = span.end();
864
865 assert!(duration.as_micros() >= 1000);
866 assert!(span.ended);
867 }
868
869 #[test]
870 fn span_child() {
871 let ctx = test_context();
872 let parent = Span::new(&ctx, "parent");
873 let child = parent.child(&ctx, "child");
874
875 assert_eq!(child.parent_id(), Some(parent.span_id()));
876 }
877
878 #[test]
879 fn span_json() {
880 let ctx = test_context();
881 let span = Span::new(&ctx, "db_query");
882
883 let json = span.to_json();
884 assert!(json.contains(r#""name":"db_query""#));
885 assert!(json.contains(r#""request_id":12345"#));
886 }
887
888 #[test]
889 fn log_config_presets() {
890 let dev = LogConfig::development();
891 assert_eq!(dev.min_level, LogLevel::Debug);
892 assert!(!dev.json_output);
893
894 let prod = LogConfig::production();
895 assert_eq!(prod.min_level, LogLevel::Info);
896 assert!(prod.json_output);
897
898 let test = LogConfig::testing();
899 assert_eq!(test.min_level, LogLevel::Trace);
900 }
901
902 #[test]
903 fn escape_json_special_chars() {
904 assert_eq!(escape_json("hello\nworld"), "hello\\nworld");
905 assert_eq!(escape_json(r#"say "hi""#), r#"say \"hi\""#);
906 assert_eq!(escape_json("tab\there"), "tab\\there");
907 }
908
909 #[test]
910 fn log_macro_basic() {
911 let ctx = test_context();
912 let entry = log_info!(&ctx, "Basic message");
913 assert_eq!(entry.level, LogLevel::Info);
914 assert_eq!(entry.message, "Basic message");
915 assert_eq!(entry.request_id, 12345);
916 }
917
918 #[test]
919 fn log_macro_with_fields() {
920 let ctx = test_context();
921 let entry = log_info!(&ctx, "With fields", user_id => 42, action => "login");
922 assert_eq!(entry.fields.len(), 2);
923 assert_eq!(entry.fields[0], ("user_id".to_string(), "42".to_string()));
924 assert_eq!(entry.fields[1], ("action".to_string(), "login".to_string()));
925 }
926
927 #[test]
928 fn log_macro_format_string() {
929 let ctx = test_context();
930 let item_id = 99;
931 let entry = log_debug!(&ctx, "Processing item {}", item_id);
932 assert_eq!(entry.level, LogLevel::Debug);
933 assert_eq!(entry.message, "Processing item 99");
934 }
935
936 #[test]
941 fn autospan_creates_and_ends_on_drop() {
942 let ctx = test_context();
943 let start = std::time::Instant::now();
944
945 {
946 let _span = AutoSpan::new(&ctx, "short_operation");
947 std::thread::sleep(std::time::Duration::from_millis(1));
948 }
950
951 let elapsed = start.elapsed();
952 assert!(elapsed.as_millis() >= 1);
954 }
955
956 #[test]
957 fn autospan_captures_request_id() {
958 let ctx = test_context();
959 let span = AutoSpan::new(&ctx, "test_span");
960
961 assert_eq!(span.ctx_request_id, 12345);
963 }
964
965 #[test]
966 fn span_nested_three_levels_deep() {
967 let ctx = test_context();
968
969 let parent = Span::new(&ctx, "level_1");
970 let child = parent.child(&ctx, "level_2");
971 let grandchild = child.child(&ctx, "level_3");
972
973 assert!(parent.parent_id().is_none());
975 assert_eq!(child.parent_id(), Some(parent.span_id()));
976 assert_eq!(grandchild.parent_id(), Some(child.span_id()));
977
978 assert_eq!(parent.name(), "level_1");
980 assert_eq!(child.name(), "level_2");
981 assert_eq!(grandchild.name(), "level_3");
982 }
983
984 #[test]
985 fn span_nested_five_levels_deep() {
986 let ctx = test_context();
987
988 let level1 = Span::new(&ctx, "request_handler");
989 let level2 = level1.child(&ctx, "middleware");
990 let level3 = level2.child(&ctx, "auth_check");
991 let level4 = level3.child(&ctx, "token_validation");
992 let level5 = level4.child(&ctx, "signature_verify");
993
994 assert!(level1.parent_id().is_none());
996 assert_eq!(level2.parent_id(), Some(level1.span_id()));
997 assert_eq!(level3.parent_id(), Some(level2.span_id()));
998 assert_eq!(level4.parent_id(), Some(level3.span_id()));
999 assert_eq!(level5.parent_id(), Some(level4.span_id()));
1000 }
1001
1002 #[test]
1003 fn span_sibling_spans_have_same_parent() {
1004 let ctx = test_context();
1005
1006 let parent = Span::new(&ctx, "parent");
1007 let sibling1 = parent.child(&ctx, "sibling_a");
1008 let sibling2 = parent.child(&ctx, "sibling_b");
1009 let sibling3 = parent.child(&ctx, "sibling_c");
1010
1011 assert_eq!(sibling1.parent_id(), Some(parent.span_id()));
1013 assert_eq!(sibling2.parent_id(), Some(parent.span_id()));
1014 assert_eq!(sibling3.parent_id(), Some(parent.span_id()));
1015
1016 assert_ne!(sibling1.span_id(), sibling2.span_id());
1018 assert_ne!(sibling2.span_id(), sibling3.span_id());
1019 assert_ne!(sibling1.span_id(), sibling3.span_id());
1020 }
1021
1022 #[test]
1023 fn span_ids_are_globally_unique() {
1024 let ctx = test_context();
1025
1026 let span1 = Span::new(&ctx, "span1");
1027 let span2 = Span::new(&ctx, "span2");
1028 let span3 = Span::new(&ctx, "span3");
1029
1030 assert_ne!(span1.span_id(), span2.span_id());
1031 assert_ne!(span2.span_id(), span3.span_id());
1032 assert_ne!(span1.span_id(), span3.span_id());
1033 }
1034
1035 #[test]
1036 fn span_request_id_propagates_to_children() {
1037 let ctx = test_context();
1038
1039 let parent = Span::new(&ctx, "parent");
1040 let child = parent.child(&ctx, "child");
1041 let grandchild = child.child(&ctx, "grandchild");
1042
1043 assert_eq!(parent.request_id, 12345);
1045 assert_eq!(child.request_id, 12345);
1046 assert_eq!(grandchild.request_id, 12345);
1047 }
1048
1049 #[test]
1050 fn span_json_includes_parent_id_when_present() {
1051 let ctx = test_context();
1052
1053 let parent = Span::new(&ctx, "parent");
1054 let child = parent.child(&ctx, "child");
1055
1056 let parent_json = parent.to_json();
1058 assert!(!parent_json.contains("parent_id"));
1059
1060 let child_json = child.to_json();
1062 assert!(child_json.contains("parent_id"));
1063 assert!(child_json.contains(&parent.span_id().to_string()));
1064 }
1065
1066 #[test]
1067 fn span_elapsed_increases_over_time() {
1068 let ctx = test_context();
1069 let span = Span::new(&ctx, "timing_test");
1070
1071 let elapsed1 = span.elapsed();
1072 std::thread::sleep(std::time::Duration::from_millis(2));
1073 let elapsed2 = span.elapsed();
1074
1075 assert!(elapsed2 > elapsed1);
1076 }
1077
1078 #[test]
1079 fn span_end_returns_final_duration() {
1080 let ctx = test_context();
1081 let mut span = Span::new(&ctx, "duration_test");
1082
1083 std::thread::sleep(std::time::Duration::from_millis(1));
1084 let duration = span.end();
1085
1086 assert!(duration.as_millis() >= 1);
1088 assert!(span.ended);
1090 }
1091
1092 #[test]
1093 fn span_multiple_end_calls_idempotent() {
1094 let ctx = test_context();
1095 let mut span = Span::new(&ctx, "multi_end_test");
1096
1097 std::thread::sleep(std::time::Duration::from_millis(1));
1098 let duration1 = span.end();
1099 let duration2 = span.end();
1100 let duration3 = span.end();
1101
1102 assert!(duration1.as_micros() > 0);
1105 assert!(duration2.as_micros() > 0);
1106 assert!(duration3.as_micros() > 0);
1107 }
1108
1109 #[test]
1110 fn span_drop_ends_span_automatically() {
1111 let ctx = test_context();
1112
1113 {
1115 let span = Span::new(&ctx, "auto_end_test");
1116 assert!(!span.ended);
1117 }
1119 }
1122
1123 #[test]
1124 fn nested_spans_timing_accumulates_correctly() {
1125 let ctx = test_context();
1126
1127 let start = std::time::Instant::now();
1128
1129 let mut parent = Span::new(&ctx, "parent");
1130 std::thread::sleep(std::time::Duration::from_millis(2));
1131
1132 {
1133 let mut child = parent.child(&ctx, "child");
1134 std::thread::sleep(std::time::Duration::from_millis(2));
1135 let child_duration = child.end();
1136
1137 assert!(child_duration.as_millis() >= 2);
1139 }
1140
1141 let parent_duration = parent.end();
1142
1143 assert!(parent_duration.as_millis() >= 4);
1145
1146 let total = start.elapsed();
1147 assert!(total.as_millis() >= 4);
1148 }
1149}