headless_lms_base/error/clean_format/
spans.rs1use tracing_error::SpanTrace;
10
11const OUR_TARGET_PREFIX: &str = "headless_lms";
13
14const MAX_FIELDS_LEN: usize = 80;
16
17#[derive(Debug, Clone)]
19pub struct SpanEntry {
20 pub name: String,
21 pub fields: String,
22}
23
24pub fn render_breadcrumb(entries: &[SpanEntry]) -> Option<String> {
26 if entries.is_empty() {
27 return None;
28 }
29 let parts: Vec<String> = entries
30 .iter()
31 .map(|entry| {
32 if entry.fields.is_empty() {
33 entry.name.clone()
34 } else {
35 format!("{}{{{}}}", entry.name, truncate(&entry.fields))
36 }
37 })
38 .collect();
39 Some(parts.join(" › "))
40}
41
42fn truncate(fields: &str) -> String {
43 if fields.chars().count() <= MAX_FIELDS_LEN {
44 return fields.to_string();
45 }
46 let cut: String = fields.chars().take(MAX_FIELDS_LEN).collect();
47 format!("{cut}…")
48}
49
50pub fn extract_spans(span_trace: &SpanTrace) -> Vec<SpanEntry> {
52 let mut entries = Vec::new();
53 span_trace.with_spans(|metadata, fields| {
54 if metadata.target().starts_with(OUR_TARGET_PREFIX) {
55 entries.push(SpanEntry {
56 name: metadata.name().to_string(),
57 fields: fields.to_string(),
58 });
59 }
60 true
61 });
62 entries.reverse();
64 entries
65}
66
67pub fn breadcrumb(span_trace: &SpanTrace) -> Option<String> {
69 render_breadcrumb(&extract_spans(span_trace))
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 fn entry(name: &str, fields: &str) -> SpanEntry {
77 SpanEntry {
78 name: name.to_string(),
79 fields: fields.to_string(),
80 }
81 }
82
83 #[test]
84 fn empty_yields_none() {
85 assert_eq!(render_breadcrumb(&[]), None);
86 }
87
88 #[test]
89 fn joins_root_first_with_fields() {
90 let entries = vec![
91 entry("http_request", "request_id=aea0"),
92 entry("send_message", "course_id=5d79"),
93 ];
94 assert_eq!(
95 render_breadcrumb(&entries).unwrap(),
96 "http_request{request_id=aea0} › send_message{course_id=5d79}"
97 );
98 }
99
100 #[test]
101 fn span_without_fields_shows_bare_name() {
102 assert_eq!(render_breadcrumb(&[entry("root", "")]).unwrap(), "root");
103 }
104
105 #[test]
106 fn long_fields_are_truncated() {
107 let long = "x".repeat(MAX_FIELDS_LEN + 20);
108 let rendered = render_breadcrumb(&[entry("s", &long)]).unwrap();
109 assert!(rendered.contains('…'), "got: {rendered}");
111 assert!(rendered.ends_with("…}"), "got: {rendered}");
112 assert!(rendered.chars().count() < long.chars().count() + "s{}".len());
113 }
114}