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 }
346 duration
347 }
348
349 #[must_use]
351 pub fn to_json(&self) -> String {
352 let duration = self.elapsed();
353 let mut json = format!(
354 r#"{{"span_id":{},"name":"{}","request_id":{},"duration_us":{}"#,
355 self.span_id,
356 escape_json(&self.name),
357 self.request_id,
358 duration.as_micros()
359 );
360
361 if let Some(parent) = self.parent_id {
362 json.push_str(&format!(r#","parent_id":{parent}"#));
363 }
364
365 json.push('}');
366 json
367 }
368}
369
370impl Drop for Span {
371 fn drop(&mut self) {
372 if !self.ended {
373 self.end();
374 }
375 }
376}
377
378pub struct AutoSpan {
383 inner: Span,
384 ctx: RequestContext,
385 config: LogConfig,
386}
387
388impl AutoSpan {
389 #[must_use]
394 pub fn new(ctx: &RequestContext, name: impl Into<String>) -> Self {
395 Self::new_with_config(ctx, LogConfig::default(), name)
396 }
397
398 #[must_use]
403 pub fn new_with_config(
404 ctx: &RequestContext,
405 config: LogConfig,
406 name: impl Into<String>,
407 ) -> Self {
408 Self {
409 inner: Span::new(ctx, name),
410 ctx: ctx.clone(),
411 config,
412 }
413 }
414}
415
416impl Drop for AutoSpan {
417 fn drop(&mut self) {
418 let duration = self.inner.end();
419 let logger = RequestLogger::new(&self.ctx, self.config.clone());
420 logger.debug_with_fields("Span ended", |e| {
421 let mut e = e.target(module_path!());
422 e = e.field("span", self.inner.name());
423 e = e.field("span_id", self.inner.span_id());
424 if let Some(parent_id) = self.inner.parent_id() {
425 e = e.field("parent_id", parent_id);
426 }
427 e.field("duration_us", duration.as_micros())
428 });
429 }
430}
431
432#[derive(Debug, Clone)]
434pub struct LogConfig {
435 pub min_level: LogLevel,
437 pub json_output: bool,
439 pub include_target: bool,
441 pub max_fields: usize,
443}
444
445impl Default for LogConfig {
446 fn default() -> Self {
447 Self {
448 min_level: LogLevel::Info,
449 json_output: true,
450 include_target: true,
451 max_fields: 16,
452 }
453 }
454}
455
456impl LogConfig {
457 #[must_use]
459 pub fn new() -> Self {
460 Self::default()
461 }
462
463 #[must_use]
465 pub fn level(mut self, level: LogLevel) -> Self {
466 self.min_level = level;
467 self
468 }
469
470 #[must_use]
472 pub fn json_output(mut self, json: bool) -> Self {
473 self.json_output = json;
474 self
475 }
476
477 #[must_use]
479 pub fn include_target(mut self, include: bool) -> Self {
480 self.include_target = include;
481 self
482 }
483
484 #[must_use]
486 pub fn max_fields(mut self, max: usize) -> Self {
487 self.max_fields = max;
488 self
489 }
490
491 #[must_use]
493 pub fn development() -> Self {
494 Self {
495 min_level: LogLevel::Debug,
496 json_output: false,
497 include_target: true,
498 max_fields: 16,
499 }
500 }
501
502 #[must_use]
504 pub fn production() -> Self {
505 Self {
506 min_level: LogLevel::Info,
507 json_output: true,
508 include_target: true,
509 max_fields: 16,
510 }
511 }
512
513 #[must_use]
515 pub fn testing() -> Self {
516 Self {
517 min_level: LogLevel::Trace,
518 json_output: true,
519 include_target: true,
520 max_fields: 16,
521 }
522 }
523}
524
525use std::sync::atomic::AtomicUsize;
530
531static GLOBAL_LOG_LEVEL: AtomicUsize = AtomicUsize::new(LogLevel::Info as usize);
536
537#[inline]
539#[must_use]
540pub fn global_log_level() -> LogLevel {
541 match GLOBAL_LOG_LEVEL.load(Ordering::Relaxed) {
542 0 => LogLevel::Trace,
543 1 => LogLevel::Debug,
544 2 => LogLevel::Info,
545 3 => LogLevel::Warn,
546 _ => LogLevel::Error,
547 }
548}
549
550pub fn set_global_log_level(level: LogLevel) {
554 GLOBAL_LOG_LEVEL.store(level as usize, Ordering::Relaxed);
555}
556
557#[inline]
559#[must_use]
560pub fn level_enabled(level: LogLevel) -> bool {
561 level >= global_log_level()
562}
563
564pub struct RequestLogger<'a> {
584 ctx: &'a RequestContext,
585 config: LogConfig,
586}
587
588impl<'a> RequestLogger<'a> {
589 #[must_use]
591 pub fn new(ctx: &'a RequestContext, config: LogConfig) -> Self {
592 Self { ctx, config }
593 }
594
595 #[inline]
597 #[must_use]
598 pub fn is_enabled(&self, level: LogLevel) -> bool {
599 level >= self.config.min_level && level_enabled(level)
600 }
601
602 pub fn emit(&self, entry: LogEntry) {
604 if !self.is_enabled(entry.level) {
605 return;
606 }
607
608 let output = if self.config.json_output {
609 entry.to_json()
610 } else {
611 entry.to_compact()
612 };
613
614 eprintln!("{output}");
617 }
618
619 pub fn trace(&self, message: impl Into<String>) {
621 if self.is_enabled(LogLevel::Trace) {
622 self.emit(LogEntry::new(self.ctx, LogLevel::Trace, message));
623 }
624 }
625
626 pub fn debug(&self, message: impl Into<String>) {
628 if self.is_enabled(LogLevel::Debug) {
629 self.emit(LogEntry::new(self.ctx, LogLevel::Debug, message));
630 }
631 }
632
633 pub fn info(&self, message: impl Into<String>) {
635 if self.is_enabled(LogLevel::Info) {
636 self.emit(LogEntry::new(self.ctx, LogLevel::Info, message));
637 }
638 }
639
640 pub fn warn(&self, message: impl Into<String>) {
642 if self.is_enabled(LogLevel::Warn) {
643 self.emit(LogEntry::new(self.ctx, LogLevel::Warn, message));
644 }
645 }
646
647 pub fn error(&self, message: impl Into<String>) {
649 if self.is_enabled(LogLevel::Error) {
650 self.emit(LogEntry::new(self.ctx, LogLevel::Error, message));
651 }
652 }
653
654 pub fn trace_with_fields<F>(&self, message: impl Into<String>, f: F)
656 where
657 F: FnOnce(LogEntry) -> LogEntry,
658 {
659 if self.is_enabled(LogLevel::Trace) {
660 let entry = f(LogEntry::new(self.ctx, LogLevel::Trace, message));
661 self.emit(entry);
662 }
663 }
664
665 pub fn debug_with_fields<F>(&self, message: impl Into<String>, f: F)
667 where
668 F: FnOnce(LogEntry) -> LogEntry,
669 {
670 if self.is_enabled(LogLevel::Debug) {
671 let entry = f(LogEntry::new(self.ctx, LogLevel::Debug, message));
672 self.emit(entry);
673 }
674 }
675
676 pub fn info_with_fields<F>(&self, message: impl Into<String>, f: F)
678 where
679 F: FnOnce(LogEntry) -> LogEntry,
680 {
681 if self.is_enabled(LogLevel::Info) {
682 let entry = f(LogEntry::new(self.ctx, LogLevel::Info, message));
683 self.emit(entry);
684 }
685 }
686
687 pub fn warn_with_fields<F>(&self, message: impl Into<String>, f: F)
689 where
690 F: FnOnce(LogEntry) -> LogEntry,
691 {
692 if self.is_enabled(LogLevel::Warn) {
693 let entry = f(LogEntry::new(self.ctx, LogLevel::Warn, message));
694 self.emit(entry);
695 }
696 }
697
698 pub fn error_with_fields<F>(&self, message: impl Into<String>, f: F)
700 where
701 F: FnOnce(LogEntry) -> LogEntry,
702 {
703 if self.is_enabled(LogLevel::Error) {
704 let entry = f(LogEntry::new(self.ctx, LogLevel::Error, message));
705 self.emit(entry);
706 }
707 }
708
709 #[must_use]
711 pub fn span(&self, name: impl Into<String>) -> Span {
712 Span::new(self.ctx, name)
713 }
714
715 #[must_use]
717 pub fn span_auto(&self, name: impl Into<String>) -> AutoSpan {
718 AutoSpan::new_with_config(self.ctx, self.config.clone(), name)
719 }
720}
721
722#[macro_export]
739macro_rules! log_trace {
740 ($ctx:expr, $msg:expr) => {
741 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Trace, $msg)
742 .target(module_path!())
743 };
744 ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
745 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Trace, $msg)
746 .target(module_path!())
747 $(.field(stringify!($key), $value))+
748 };
749 ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
750 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Trace, format!($fmt, $($arg)*))
751 .target(module_path!())
752 };
753}
754
755#[macro_export]
757macro_rules! log_debug {
758 ($ctx:expr, $msg:expr) => {
759 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Debug, $msg)
760 .target(module_path!())
761 };
762 ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
763 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Debug, $msg)
764 .target(module_path!())
765 $(.field(stringify!($key), $value))+
766 };
767 ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
768 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Debug, format!($fmt, $($arg)*))
769 .target(module_path!())
770 };
771}
772
773#[macro_export]
775macro_rules! log_info {
776 ($ctx:expr, $msg:expr) => {
777 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Info, $msg)
778 .target(module_path!())
779 };
780 ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
781 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Info, $msg)
782 .target(module_path!())
783 $(.field(stringify!($key), $value))+
784 };
785 ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
786 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Info, format!($fmt, $($arg)*))
787 .target(module_path!())
788 };
789}
790
791#[macro_export]
793macro_rules! log_warn {
794 ($ctx:expr, $msg:expr) => {
795 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Warn, $msg)
796 .target(module_path!())
797 };
798 ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
799 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Warn, $msg)
800 .target(module_path!())
801 $(.field(stringify!($key), $value))+
802 };
803 ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
804 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Warn, format!($fmt, $($arg)*))
805 .target(module_path!())
806 };
807}
808
809#[macro_export]
811macro_rules! log_error {
812 ($ctx:expr, $msg:expr) => {
813 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Error, $msg)
814 .target(module_path!())
815 };
816 ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
817 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Error, $msg)
818 .target(module_path!())
819 $(.field(stringify!($key), $value))+
820 };
821 ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
822 $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Error, format!($fmt, $($arg)*))
823 .target(module_path!())
824 };
825}
826
827#[cfg(test)]
828mod tests {
829 use super::*;
830 use asupersync::Cx;
831
832 fn test_context() -> crate::context::RequestContext {
833 let cx = Cx::for_testing();
834 crate::context::RequestContext::new(cx, 12345)
835 }
836
837 #[test]
838 fn log_level_ordering() {
839 assert!(LogLevel::Trace < LogLevel::Debug);
840 assert!(LogLevel::Debug < LogLevel::Info);
841 assert!(LogLevel::Info < LogLevel::Warn);
842 assert!(LogLevel::Warn < LogLevel::Error);
843 }
844
845 #[test]
846 fn log_level_display() {
847 assert_eq!(LogLevel::Info.as_str(), "info");
848 assert_eq!(LogLevel::Error.as_char(), 'E');
849 }
850
851 #[test]
852 fn log_entry_json() {
853 let ctx = test_context();
854 let entry = LogEntry::new(&ctx, LogLevel::Info, "Test message")
855 .target("test::module")
856 .field("user_id", 42)
857 .field("action", "login");
858
859 let json = entry.to_json();
860 assert!(json.contains(r#""level":"info""#));
861 assert!(json.contains(r#""message":"Test message""#));
862 assert!(json.contains(r#""request_id":12345"#));
863 assert!(json.contains(r#""target":"test::module""#));
864 assert!(json.contains(r#""user_id":"42""#));
865 assert!(json.contains(r#""action":"login""#));
866 }
867
868 #[test]
869 fn log_entry_compact() {
870 let ctx = test_context();
871 let entry =
872 LogEntry::new(&ctx, LogLevel::Warn, "Something happened").field("error_code", "E001");
873
874 let compact = entry.to_compact();
875 assert!(compact.starts_with("[W] req=12345"));
876 assert!(compact.contains("Something happened"));
877 assert!(compact.contains("error_code=E001"));
878 }
879
880 #[test]
881 fn span_timing() {
882 let ctx = test_context();
883 let mut span = Span::new(&ctx, "test_operation");
884
885 std::thread::sleep(std::time::Duration::from_millis(1));
886 let duration = span.end();
887
888 assert!(duration.as_micros() >= 1000);
889 assert!(span.ended);
890 }
891
892 #[test]
893 fn span_child() {
894 let ctx = test_context();
895 let parent = Span::new(&ctx, "parent");
896 let child = parent.child(&ctx, "child");
897
898 assert_eq!(child.parent_id(), Some(parent.span_id()));
899 }
900
901 #[test]
902 fn span_json() {
903 let ctx = test_context();
904 let span = Span::new(&ctx, "db_query");
905
906 let json = span.to_json();
907 assert!(json.contains(r#""name":"db_query""#));
908 assert!(json.contains(r#""request_id":12345"#));
909 }
910
911 #[test]
912 fn log_config_presets() {
913 let dev = LogConfig::development();
914 assert_eq!(dev.min_level, LogLevel::Debug);
915 assert!(!dev.json_output);
916
917 let prod = LogConfig::production();
918 assert_eq!(prod.min_level, LogLevel::Info);
919 assert!(prod.json_output);
920
921 let test = LogConfig::testing();
922 assert_eq!(test.min_level, LogLevel::Trace);
923 }
924
925 #[test]
926 fn escape_json_special_chars() {
927 assert_eq!(escape_json("hello\nworld"), "hello\\nworld");
928 assert_eq!(escape_json(r#"say "hi""#), r#"say \"hi\""#);
929 assert_eq!(escape_json("tab\there"), "tab\\there");
930 }
931
932 #[test]
933 fn log_macro_basic() {
934 let ctx = test_context();
935 let entry = log_info!(&ctx, "Basic message");
936 assert_eq!(entry.level, LogLevel::Info);
937 assert_eq!(entry.message, "Basic message");
938 assert_eq!(entry.request_id, 12345);
939 }
940
941 #[test]
942 fn log_macro_with_fields() {
943 let ctx = test_context();
944 let entry = log_info!(&ctx, "With fields", user_id => 42, action => "login");
945 assert_eq!(entry.fields.len(), 2);
946 assert_eq!(entry.fields[0], ("user_id".to_string(), "42".to_string()));
947 assert_eq!(entry.fields[1], ("action".to_string(), "login".to_string()));
948 }
949
950 #[test]
951 fn log_macro_format_string() {
952 let ctx = test_context();
953 let item_id = 99;
954 let entry = log_debug!(&ctx, "Processing item {}", item_id);
955 assert_eq!(entry.level, LogLevel::Debug);
956 assert_eq!(entry.message, "Processing item 99");
957 }
958
959 #[test]
964 fn autospan_creates_and_ends_on_drop() {
965 let ctx = test_context();
966 let start = std::time::Instant::now();
967
968 {
969 let _span = AutoSpan::new(&ctx, "short_operation");
970 std::thread::sleep(std::time::Duration::from_millis(1));
971 }
973
974 let elapsed = start.elapsed();
975 assert!(elapsed.as_millis() >= 1);
977 }
978
979 #[test]
980 fn autospan_captures_request_id() {
981 let ctx = test_context();
982 let span = AutoSpan::new(&ctx, "test_span");
983
984 assert_eq!(span.ctx.request_id(), 12345);
986 }
987
988 #[test]
989 fn span_nested_three_levels_deep() {
990 let ctx = test_context();
991
992 let parent = Span::new(&ctx, "level_1");
993 let child = parent.child(&ctx, "level_2");
994 let grandchild = child.child(&ctx, "level_3");
995
996 assert!(parent.parent_id().is_none());
998 assert_eq!(child.parent_id(), Some(parent.span_id()));
999 assert_eq!(grandchild.parent_id(), Some(child.span_id()));
1000
1001 assert_eq!(parent.name(), "level_1");
1003 assert_eq!(child.name(), "level_2");
1004 assert_eq!(grandchild.name(), "level_3");
1005 }
1006
1007 #[test]
1008 fn span_nested_five_levels_deep() {
1009 let ctx = test_context();
1010
1011 let level1 = Span::new(&ctx, "request_handler");
1012 let level2 = level1.child(&ctx, "middleware");
1013 let level3 = level2.child(&ctx, "auth_check");
1014 let level4 = level3.child(&ctx, "token_validation");
1015 let level5 = level4.child(&ctx, "signature_verify");
1016
1017 assert!(level1.parent_id().is_none());
1019 assert_eq!(level2.parent_id(), Some(level1.span_id()));
1020 assert_eq!(level3.parent_id(), Some(level2.span_id()));
1021 assert_eq!(level4.parent_id(), Some(level3.span_id()));
1022 assert_eq!(level5.parent_id(), Some(level4.span_id()));
1023 }
1024
1025 #[test]
1026 fn span_sibling_spans_have_same_parent() {
1027 let ctx = test_context();
1028
1029 let parent = Span::new(&ctx, "parent");
1030 let sibling1 = parent.child(&ctx, "sibling_a");
1031 let sibling2 = parent.child(&ctx, "sibling_b");
1032 let sibling3 = parent.child(&ctx, "sibling_c");
1033
1034 assert_eq!(sibling1.parent_id(), Some(parent.span_id()));
1036 assert_eq!(sibling2.parent_id(), Some(parent.span_id()));
1037 assert_eq!(sibling3.parent_id(), Some(parent.span_id()));
1038
1039 assert_ne!(sibling1.span_id(), sibling2.span_id());
1041 assert_ne!(sibling2.span_id(), sibling3.span_id());
1042 assert_ne!(sibling1.span_id(), sibling3.span_id());
1043 }
1044
1045 #[test]
1046 fn span_ids_are_globally_unique() {
1047 let ctx = test_context();
1048
1049 let span1 = Span::new(&ctx, "span1");
1050 let span2 = Span::new(&ctx, "span2");
1051 let span3 = Span::new(&ctx, "span3");
1052
1053 assert_ne!(span1.span_id(), span2.span_id());
1054 assert_ne!(span2.span_id(), span3.span_id());
1055 assert_ne!(span1.span_id(), span3.span_id());
1056 }
1057
1058 #[test]
1059 fn span_request_id_propagates_to_children() {
1060 let ctx = test_context();
1061
1062 let parent = Span::new(&ctx, "parent");
1063 let child = parent.child(&ctx, "child");
1064 let grandchild = child.child(&ctx, "grandchild");
1065
1066 assert_eq!(parent.request_id, 12345);
1068 assert_eq!(child.request_id, 12345);
1069 assert_eq!(grandchild.request_id, 12345);
1070 }
1071
1072 #[test]
1073 fn span_json_includes_parent_id_when_present() {
1074 let ctx = test_context();
1075
1076 let parent = Span::new(&ctx, "parent");
1077 let child = parent.child(&ctx, "child");
1078
1079 let parent_json = parent.to_json();
1081 assert!(!parent_json.contains("parent_id"));
1082
1083 let child_json = child.to_json();
1085 assert!(child_json.contains("parent_id"));
1086 assert!(child_json.contains(&parent.span_id().to_string()));
1087 }
1088
1089 #[test]
1090 fn span_elapsed_increases_over_time() {
1091 let ctx = test_context();
1092 let span = Span::new(&ctx, "timing_test");
1093
1094 let elapsed1 = span.elapsed();
1095 std::thread::sleep(std::time::Duration::from_millis(2));
1096 let elapsed2 = span.elapsed();
1097
1098 assert!(elapsed2 > elapsed1);
1099 }
1100
1101 #[test]
1102 fn span_end_returns_final_duration() {
1103 let ctx = test_context();
1104 let mut span = Span::new(&ctx, "duration_test");
1105
1106 std::thread::sleep(std::time::Duration::from_millis(1));
1107 let duration = span.end();
1108
1109 assert!(duration.as_millis() >= 1);
1111 assert!(span.ended);
1113 }
1114
1115 #[test]
1116 fn span_multiple_end_calls_idempotent() {
1117 let ctx = test_context();
1118 let mut span = Span::new(&ctx, "multi_end_test");
1119
1120 std::thread::sleep(std::time::Duration::from_millis(1));
1121 let duration1 = span.end();
1122 let duration2 = span.end();
1123 let duration3 = span.end();
1124
1125 assert!(duration1.as_micros() > 0);
1128 assert!(duration2.as_micros() > 0);
1129 assert!(duration3.as_micros() > 0);
1130 }
1131
1132 #[test]
1133 fn span_drop_ends_span_automatically() {
1134 let ctx = test_context();
1135
1136 {
1138 let span = Span::new(&ctx, "auto_end_test");
1139 assert!(!span.ended);
1140 }
1142 }
1145
1146 #[test]
1147 fn nested_spans_timing_accumulates_correctly() {
1148 let ctx = test_context();
1149
1150 let start = std::time::Instant::now();
1151
1152 let mut parent = Span::new(&ctx, "parent");
1153 std::thread::sleep(std::time::Duration::from_millis(2));
1154
1155 {
1156 let mut child = parent.child(&ctx, "child");
1157 std::thread::sleep(std::time::Duration::from_millis(2));
1158 let child_duration = child.end();
1159
1160 assert!(child_duration.as_millis() >= 2);
1162 }
1163
1164 let parent_duration = parent.end();
1165
1166 assert!(parent_duration.as_millis() >= 4);
1168
1169 let total = start.elapsed();
1170 assert!(total.as_millis() >= 4);
1171 }
1172}