rivet_logger/processors/
introspection.rs1#[cfg(not(target_arch = "wasm32"))]
2use std::path::Path;
3
4#[cfg(not(target_arch = "wasm32"))]
5use backtrace::Backtrace;
6
7use crate::logger::{BoxError, Level, LogRecord, LogValue, Processor};
8
9#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
10pub struct Introspection {
11 min_level: Level,
12 skip_module_partials: Vec<String>,
13 skip_stack_frames: usize,
14}
15
16impl Introspection {
17 pub fn new(
18 min_level: Level,
19 skip_module_partials: Vec<String>,
20 skip_stack_frames: usize,
21 ) -> Self {
22 let mut defaults = vec!["rivet_logger::".to_string()];
23 defaults.extend(skip_module_partials);
24
25 Self {
26 min_level,
27 skip_module_partials: defaults,
28 skip_stack_frames,
29 }
30 }
31
32 #[cfg(not(target_arch = "wasm32"))]
33 fn should_skip(&self, module: &str) -> bool {
34 self.skip_module_partials
35 .iter()
36 .any(|partial| module.contains(partial))
37 }
38}
39
40impl Default for Introspection {
41 fn default() -> Self {
42 Self::new(Level::Debug, Vec::new(), 0)
43 }
44}
45
46impl Processor for Introspection {
47 #[cfg(target_arch = "wasm32")]
48 fn process(&self, mut record: LogRecord) -> Result<LogRecord, BoxError> {
49 if record.level < self.min_level {
50 return Ok(record);
51 }
52
53 let _ = self.skip_stack_frames;
54 record.extra.insert("file".to_string(), LogValue::Null);
55 record.extra.insert("line".to_string(), LogValue::Null);
56 record.extra.insert("module".to_string(), LogValue::Null);
57 record.extra.insert("function".to_string(), LogValue::Null);
58 Ok(record)
59 }
60
61 #[cfg(not(target_arch = "wasm32"))]
62 fn process(&self, mut record: LogRecord) -> Result<LogRecord, BoxError> {
63 if record.level < self.min_level {
64 return Ok(record);
65 }
66
67 let trace = Backtrace::new();
68 let mut candidates = Vec::new();
69
70 for frame in trace.frames() {
71 for symbol in frame.symbols() {
72 let name = symbol
73 .name()
74 .map(|value| value.to_string())
75 .unwrap_or_default();
76 if name.is_empty() {
77 continue;
78 }
79 if self.should_skip(&name) {
80 continue;
81 }
82
83 let module = module_from_symbol(&name).unwrap_or_default();
84 let function = function_from_symbol(&name).unwrap_or_default();
85 let file = symbol.filename().map(path_to_string).unwrap_or_default();
86 let line = symbol.lineno().map(u64::from);
87
88 candidates.push((file, line, module, function));
89 }
90 }
91
92 let selected = candidates.get(self.skip_stack_frames);
93 if let Some((file, line, module, function)) = selected {
94 record
95 .extra
96 .insert("file".to_string(), nullable_string(file.clone()));
97 record.extra.insert(
98 "line".to_string(),
99 line.map(LogValue::U64).unwrap_or(LogValue::Null),
100 );
101 record
102 .extra
103 .insert("module".to_string(), nullable_string(module.clone()));
104 record
105 .extra
106 .insert("function".to_string(), nullable_string(function.clone()));
107 } else {
108 record.extra.insert("file".to_string(), LogValue::Null);
109 record.extra.insert("line".to_string(), LogValue::Null);
110 record.extra.insert("module".to_string(), LogValue::Null);
111 record.extra.insert("function".to_string(), LogValue::Null);
112 }
113
114 Ok(record)
115 }
116}
117
118#[cfg(not(target_arch = "wasm32"))]
119fn nullable_string(value: String) -> LogValue {
120 if value.is_empty() {
121 LogValue::Null
122 } else {
123 LogValue::String(value)
124 }
125}
126
127#[cfg(not(target_arch = "wasm32"))]
128fn module_from_symbol(symbol: &str) -> Option<String> {
129 symbol
130 .rsplit_once("::")
131 .map(|(module, _)| module.to_string())
132}
133
134#[cfg(not(target_arch = "wasm32"))]
135fn function_from_symbol(symbol: &str) -> Option<String> {
136 symbol
137 .rsplit_once("::")
138 .map(|(_, function)| function.to_string())
139}
140
141#[cfg(not(target_arch = "wasm32"))]
142fn path_to_string(path: &Path) -> String {
143 path.to_string_lossy().to_string()
144}
145
146#[cfg(test)]
147mod tests {
148 use std::collections::BTreeMap;
149
150 use time::OffsetDateTime;
151
152 use crate::logger::LogRecord;
153
154 use super::*;
155
156 #[test]
157 fn injects_trace_fields() {
158 let processor = Introspection::default();
159 let record = LogRecord {
160 datetime: OffsetDateTime::now_utc(),
161 channel: "test".to_string(),
162 level: Level::Info,
163 message: "hello".to_string(),
164 context: BTreeMap::new(),
165 extra: BTreeMap::new(),
166 };
167
168 let processed = processor.process(record).expect("processor should run");
169
170 assert!(processed.extra.contains_key("file"));
171 assert!(processed.extra.contains_key("line"));
172 assert!(processed.extra.contains_key("module"));
173 assert!(processed.extra.contains_key("function"));
174 }
175}