Skip to main content

rivet_logger/processors/
introspection.rs

1#[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}