1use alloc::boxed::Box;
4use alloc::vec::Vec;
5use core::fmt;
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
18#[repr(u8)]
19pub enum TracingLevel {
20 TRACE = 0,
22 DEBUG = 1,
24 INFO = 2,
26 WARN = 3,
28 ERROR = 4,
30 UNKNOWN = 5,
32}
33
34impl fmt::Display for TracingLevel {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 f.write_str(match self {
37 Self::TRACE => "TRACE",
38 Self::DEBUG => "DEBUG",
39 Self::INFO => "INFO",
40 Self::WARN => "WARN",
41 Self::ERROR => "ERROR",
42 Self::UNKNOWN => "UNKNOWN",
43 })
44 }
45}
46
47impl core::str::FromStr for TracingLevel {
48 type Err = core::convert::Infallible;
49
50 fn from_str(s: &str) -> Result<Self, Self::Err> {
51 let level = if s.eq_ignore_ascii_case("TRACE") {
52 Self::TRACE
53 } else if s.eq_ignore_ascii_case("DEBUG") {
54 Self::DEBUG
55 } else if s.eq_ignore_ascii_case("INFO") {
56 Self::INFO
57 } else if s.eq_ignore_ascii_case("WARN") {
58 Self::WARN
59 } else if s.eq_ignore_ascii_case("ERROR") {
60 Self::ERROR
61 } else {
62 Self::UNKNOWN
63 };
64 Ok(level)
65 }
66}
67
68impl<'de> Deserialize<'de> for TracingLevel {
69 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
70 where
71 D: serde::Deserializer<'de>,
72 {
73 let s = alloc::borrow::Cow::<str>::deserialize(deserializer)?;
76 Ok(s.parse().unwrap_or(Self::UNKNOWN))
77 }
78}
79
80#[cfg(feature = "tracing")]
81impl From<TracingLevel> for tracing::Level {
82 #[inline]
83 fn from(level: TracingLevel) -> Self {
84 match level {
85 TracingLevel::ERROR | TracingLevel::UNKNOWN => Self::ERROR,
86 TracingLevel::WARN => Self::WARN,
87 TracingLevel::INFO => Self::INFO,
88 TracingLevel::DEBUG => Self::DEBUG,
89 TracingLevel::TRACE => Self::TRACE,
90 }
91 }
92}
93
94#[cfg(feature = "tracing")]
95impl From<&tracing::Level> for TracingLevel {
96 #[inline]
97 fn from(level: &tracing::Level) -> Self {
98 match *level {
99 tracing::Level::ERROR => Self::ERROR,
100 tracing::Level::WARN => Self::WARN,
101 tracing::Level::INFO => Self::INFO,
102 tracing::Level::DEBUG => Self::DEBUG,
103 tracing::Level::TRACE => Self::TRACE,
104 }
105 }
106}
107
108#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
114#[non_exhaustive]
115pub struct ErasedSpanTrace {
116 spans: Vec<ErasedSpan>,
117}
118
119#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
121#[non_exhaustive]
122pub struct ErasedSpan {
123 metadata: ErasedMetadata,
124 fields: Box<str>,
125}
126
127impl ErasedSpan {
128 #[must_use]
130 #[inline]
131 pub const fn metadata(&self) -> &ErasedMetadata {
132 &self.metadata
133 }
134
135 #[must_use]
137 #[inline]
138 pub fn fields(&self) -> &str {
139 &self.fields
140 }
141}
142
143#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
145#[non_exhaustive]
146pub struct ErasedMetadata {
147 name: Box<str>,
148 target: Box<str>,
149 level: TracingLevel,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 module_path: Option<Box<str>>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 file: Option<Box<str>>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 line: Option<u32>,
156}
157
158impl ErasedMetadata {
159 #[must_use]
161 #[inline]
162 pub fn name(&self) -> &str {
163 &self.name
164 }
165
166 #[must_use]
168 #[inline]
169 pub fn target(&self) -> &str {
170 &self.target
171 }
172
173 #[must_use]
175 #[inline]
176 pub const fn level(&self) -> TracingLevel {
177 self.level
178 }
179
180 #[must_use]
182 #[inline]
183 pub fn module_path(&self) -> Option<&str> {
184 self.module_path.as_deref()
185 }
186
187 #[must_use]
189 #[inline]
190 pub fn file(&self) -> Option<&str> {
191 self.file.as_deref()
192 }
193
194 #[must_use]
196 #[inline]
197 pub const fn line(&self) -> Option<u32> {
198 self.line
199 }
200}
201
202#[cfg(feature = "tracing")]
203impl<'a> From<&'a tracing::Metadata<'_>> for ErasedMetadata {
204 fn from(meta: &'a tracing::Metadata<'_>) -> Self {
205 Self {
206 name: meta.name().into(),
207 target: meta.target().into(),
208 level: meta.level().into(),
209 module_path: meta.module_path().map(Box::from),
210 file: meta.file().map(Box::from),
211 line: meta.line(),
212 }
213 }
214}
215
216impl ErasedSpanTrace {
217 #[cfg(feature = "tracing")]
219 #[must_use]
220 pub fn from_spantrace_ref(st: &crate::SpanTrace) -> Self {
221 let mut spans = Vec::new();
222 st.as_span_trace().with_spans(|metadata, fields| {
223 spans.push(ErasedSpan {
224 metadata: ErasedMetadata::from(metadata),
225 fields: fields.into(),
226 });
227 true
228 });
229 Self { spans }
230 }
231
232 #[must_use]
234 #[inline]
235 pub const fn is_empty(&self) -> bool {
236 self.spans.is_empty()
237 }
238
239 pub fn with_spans<F>(&self, mut f: F)
241 where
242 F: FnMut(&ErasedMetadata, &str) -> bool,
243 {
244 for ErasedSpan { metadata, fields } in &self.spans {
245 if !f(metadata, fields) {
246 break;
247 }
248 }
249 }
250}
251
252#[cfg(feature = "tracing")]
253impl From<&crate::SpanTrace> for ErasedSpanTrace {
254 #[inline]
255 fn from(st: &crate::SpanTrace) -> Self {
256 Self::from_spantrace_ref(st)
257 }
258}
259
260impl fmt::Display for ErasedSpanTrace {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 let mut err = Ok(());
263 let mut span = 0;
264
265 macro_rules! try_bool {
266 ($e:expr, $dest:ident) => {{
267 let ret = $e.unwrap_or_else(|e| $dest = Err(e));
268
269 if $dest.is_err() {
270 return false;
271 }
272
273 ret
274 }};
275 }
276
277 self.with_spans(|metadata, fields| {
278 if span > 0 {
279 try_bool!(write!(f, "\n",), err);
280 }
281
282 try_bool!(
283 write!(
284 f,
285 "{:>3}: {}::{}",
286 span + 1,
287 metadata.target(),
288 metadata.name()
289 ),
290 err
291 );
292
293 if !fields.is_empty() {
294 try_bool!(write!(f, "\n with {}", fields), err);
295 }
296
297 if let Some((file, line)) = metadata.file().zip(metadata.line()) {
298 try_bool!(write!(f, "\n at {}:{}", file, line), err);
299 }
300
301 span += 1;
302 true
303 });
304
305 err
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn test_tracing_level_tolerant_deserialization() {
315 let level: TracingLevel = serde_json::from_value(serde_json::json!("info")).unwrap();
316 assert_eq!(level, TracingLevel::INFO, "case-insensitive known level");
317
318 let level: TracingLevel = serde_json::from_value(serde_json::json!("FATAL")).unwrap();
319 assert_eq!(
320 level,
321 TracingLevel::UNKNOWN,
322 "unknown level degrades to UNKNOWN"
323 );
324
325 let json = serde_json::to_string(&TracingLevel::WARN).unwrap();
327 assert_eq!(json, r#""WARN""#);
328 let back: TracingLevel = serde_json::from_str(&json).unwrap();
329 assert_eq!(back, TracingLevel::WARN);
330
331 let json = serde_json::to_string(&TracingLevel::UNKNOWN).unwrap();
333 assert_eq!(json, r#""UNKNOWN""#);
334 let back: TracingLevel = serde_json::from_str(&json).unwrap();
335 assert_eq!(back, TracingLevel::UNKNOWN);
336 }
337
338 #[test]
339 fn test_erased_spantrace_deserialization() {
340 let json = serde_json::json!({
341 "spans": [
342 {
343 "metadata": {
344 "name": "inner_span",
345 "target": "my_crate::module",
346 "level": "INFO",
347 "module_path": "my_crate::module",
348 "file": "src/lib.rs",
349 "line": 42
350 },
351 "fields": "request_id=123"
352 },
353 {
354 "metadata": {
355 "name": "outer_span",
356 "target": "my_crate",
357 "level": "DEBUG"
358 },
359 "fields": ""
360 }
361 ]
362 });
363
364 let spantrace: ErasedSpanTrace = serde_json::from_value(json).unwrap();
365 assert_eq!(spantrace.spans.len(), 2);
366 }
367
368 #[test]
369 fn test_erased_spantrace_serialization_roundtrip() {
370 let json = serde_json::json!({
371 "spans": [
372 {
373 "metadata": {
374 "name": "test_span",
375 "target": "test_target",
376 "level": "WARN",
377 "file": "test.rs",
378 "line": 100
379 },
380 "fields": "key=value"
381 }
382 ]
383 });
384
385 let spantrace: ErasedSpanTrace = serde_json::from_value(json).unwrap();
386 let serialized = serde_json::to_string(&spantrace).unwrap();
387 let roundtrip: ErasedSpanTrace = serde_json::from_str(&serialized).unwrap();
388
389 let serialized2 = serde_json::to_string(&roundtrip).unwrap();
391 assert_eq!(serialized, serialized2);
392 }
393
394 #[cfg(feature = "test-utils")]
395 #[test]
396 fn test_erased_spantrace_display() {
397 let json = serde_json::json!({
398 "spans": [
399 {
400 "metadata": {
401 "name": "inner_function",
402 "target": "my_module",
403 "level": "INFO",
404 "file": "src/lib.rs",
405 "line": 42
406 },
407 "fields": ""
408 },
409 {
410 "metadata": {
411 "name": "outer_function",
412 "target": "my_crate",
413 "level": "DEBUG"
414 },
415 "fields": ""
416 }
417 ]
418 });
419
420 let spantrace: ErasedSpanTrace = serde_json::from_value(json).unwrap();
421 let display = spantrace.to_string();
422
423 insta::assert_snapshot!(display, @"
424 1: my_module::inner_function
425 at src/lib.rs:42
426 2: my_crate::outer_function
427 ");
428 }
429
430 #[cfg(feature = "test-utils")]
431 #[test]
432 fn test_erased_spantrace_display_with_fields() {
433 let json = serde_json::json!({
434 "spans": [
435 {
436 "metadata": {
437 "name": "process_request",
438 "target": "server",
439 "level": "INFO",
440 "file": "src/server.rs",
441 "line": 100
442 },
443 "fields": "user_id=42, method=GET"
444 }
445 ]
446 });
447
448 let spantrace: ErasedSpanTrace = serde_json::from_value(json).unwrap();
449 let display = spantrace.to_string();
450
451 insta::assert_snapshot!(display, @"
452 1: server::process_request
453 with user_id=42, method=GET
454 at src/server.rs:100
455 ");
456 }
457
458 #[test]
459 fn test_erased_spantrace_with_spans_iteration() {
460 let json = serde_json::json!({
461 "spans": [
462 {"metadata": {"name": "span_a", "target": "target_a", "level": "INFO"}, "fields": ""},
463 {"metadata": {"name": "span_b", "target": "target_b", "level": "DEBUG"}, "fields": ""},
464 {"metadata": {"name": "span_c", "target": "target_c", "level": "TRACE"}, "fields": ""}
465 ]
466 });
467
468 let spantrace: ErasedSpanTrace = serde_json::from_value(json).unwrap();
469
470 let mut collected = Vec::new();
471 spantrace.with_spans(|metadata, _fields| {
472 collected.push(format!("{}::{}", metadata.target(), metadata.name()));
473 true });
475
476 assert_eq!(
477 collected,
478 vec!["target_a::span_a", "target_b::span_b", "target_c::span_c"]
479 );
480 }
481
482 #[test]
483 fn test_erased_spantrace_with_spans_early_exit() {
484 let json = serde_json::json!({
485 "spans": [
486 {"metadata": {"name": "a", "target": "t", "level": "INFO"}, "fields": ""},
487 {"metadata": {"name": "b", "target": "t", "level": "INFO"}, "fields": ""},
488 {"metadata": {"name": "c", "target": "t", "level": "INFO"}, "fields": ""}
489 ]
490 });
491
492 let spantrace: ErasedSpanTrace = serde_json::from_value(json).unwrap();
493
494 let mut count = 0;
495 spantrace.with_spans(|_metadata, _fields| {
496 count += 1;
497 count < 2 });
499
500 assert_eq!(count, 2);
501 }
502
503 #[test]
506 fn test_erased_metadata_getters_with_optional_fields() {
507 let json = serde_json::json!({
508 "spans": [{
509 "metadata": {
510 "name": "my_span",
511 "target": "my_crate::module",
512 "level": "INFO",
513 "module_path": "my_crate::module",
514 "file": "src/lib.rs",
515 "line": 42
516 },
517 "fields": ""
518 }]
519 });
520
521 let spantrace: ErasedSpanTrace = serde_json::from_value(json).unwrap();
522 spantrace.with_spans(|metadata, _fields| {
523 assert_eq!(metadata.module_path(), Some("my_crate::module"));
524 assert_eq!(metadata.file(), Some("src/lib.rs"));
525 assert_eq!(metadata.line(), Some(42));
526 false
527 });
528 }
529
530 #[test]
531 fn test_erased_metadata_getters_without_optional_fields() {
532 let json = serde_json::json!({
533 "spans": [{
534 "metadata": {
535 "name": "bare_span",
536 "target": "some_target",
537 "level": "DEBUG"
538 },
539 "fields": ""
540 }]
541 });
542
543 let spantrace: ErasedSpanTrace = serde_json::from_value(json).unwrap();
544 spantrace.with_spans(|metadata, _fields| {
545 assert_eq!(metadata.module_path(), None);
546 assert_eq!(metadata.file(), None);
547 assert_eq!(metadata.line(), None);
548 false
549 });
550 }
551
552 #[test]
555 fn test_erased_spantrace_display_span_zero_has_no_leading_newline() {
556 let json = serde_json::json!({
557 "spans": [{
558 "metadata": {
559 "name": "only_span",
560 "target": "t",
561 "level": "INFO"
562 },
563 "fields": ""
564 }]
565 });
566 let spantrace: ErasedSpanTrace = serde_json::from_value(json).unwrap();
567 let display = spantrace.to_string();
568 assert!(!display.starts_with('\n'));
570 assert!(display.starts_with(" 1:"));
571 }
572}