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