Skip to main content

mars_xlog/
tracing_layer.rs

1//! `tracing` layer that forwards events into Mars Xlog.
2//!
3//! This module is gated behind the `tracing` feature.
4use crate::{LogLevel, Xlog};
5use std::fmt;
6use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
7use std::sync::Arc;
8use tracing::field::{Field, Visit};
9use tracing::{Event, Level, Metadata, Subscriber};
10use tracing_subscriber::layer::{Context, Layer};
11use tracing_subscriber::registry::LookupSpan;
12
13/// Configuration for `XlogLayer`.
14#[derive(Debug, Clone)]
15pub struct XlogLayerConfig {
16    /// Whether the layer should emit logs.
17    pub enabled: bool,
18    /// Minimum log level to forward.
19    pub level: LogLevel,
20    /// Optional tag override (defaults to `Metadata::target()`).
21    pub tag: Option<String>,
22    /// Include span names in the formatted message.
23    pub include_spans: bool,
24}
25
26impl XlogLayerConfig {
27    /// Create a layer config with `enabled = true`, no tag override, and
28    /// span names excluded from the formatted message.
29    pub fn new(level: LogLevel) -> Self {
30        Self {
31            enabled: true,
32            level,
33            tag: None,
34            include_spans: false,
35        }
36    }
37
38    /// Enable or disable the layer.
39    pub fn enabled(mut self, enabled: bool) -> Self {
40        self.enabled = enabled;
41        self
42    }
43
44    /// Set the minimum forwarded log level.
45    pub fn level(mut self, level: LogLevel) -> Self {
46        self.level = level;
47        self
48    }
49
50    /// Override the emitted tag instead of using `Metadata::target()`.
51    pub fn tag(mut self, tag: impl Into<String>) -> Self {
52        self.tag = Some(tag.into());
53        self
54    }
55
56    /// Include the current span stack in the formatted message body.
57    pub fn include_spans(mut self, include: bool) -> Self {
58        self.include_spans = include;
59        self
60    }
61}
62
63/// Handle used to toggle a running `XlogLayer`.
64#[derive(Clone)]
65pub struct XlogLayerHandle {
66    state: Arc<LayerState>,
67}
68
69impl XlogLayerHandle {
70    /// Enable or disable forwarding.
71    pub fn set_enabled(&self, enabled: bool) {
72        self.state.enabled.store(enabled, Ordering::Release);
73    }
74
75    /// Check whether forwarding is enabled.
76    pub fn enabled(&self) -> bool {
77        self.state.enabled.load(Ordering::Acquire)
78    }
79
80    /// Update the minimum forwarded level for this layer only.
81    pub fn set_level(&self, level: LogLevel) {
82        self.state
83            .level
84            .store(level_to_u8(level), Ordering::Release);
85    }
86
87    /// Read the current minimum log level.
88    pub fn level(&self) -> LogLevel {
89        level_from_u8(self.state.level.load(Ordering::Acquire))
90    }
91}
92
93/// `tracing-subscriber` layer that forwards events to a `Xlog` instance.
94pub struct XlogLayer {
95    state: Arc<LayerState>,
96    tag: Option<String>,
97    include_spans: bool,
98}
99
100impl XlogLayer {
101    /// Build a layer using the logger's current level and defaults.
102    pub fn new(logger: Xlog) -> (Self, XlogLayerHandle) {
103        let config = XlogLayerConfig::new(logger.level());
104        Self::with_config(logger, config)
105    }
106
107    /// Build a layer from explicit configuration.
108    ///
109    /// This only configures layer-side filtering and does not mutate the
110    /// underlying logger's level.
111    pub fn with_config(logger: Xlog, config: XlogLayerConfig) -> (Self, XlogLayerHandle) {
112        let state = Arc::new(LayerState::new(logger, config.enabled, config.level));
113        let layer = Self {
114            state: Arc::clone(&state),
115            tag: config.tag,
116            include_spans: config.include_spans,
117        };
118        let handle = XlogLayerHandle { state };
119        (layer, handle)
120    }
121
122    /// Create a new handle that can be used to reconfigure the layer.
123    pub fn handle(&self) -> XlogLayerHandle {
124        XlogLayerHandle {
125            state: Arc::clone(&self.state),
126        }
127    }
128
129    fn is_enabled_for(&self, level: LogLevel) -> bool {
130        if !self.state.enabled.load(Ordering::Acquire) {
131            return false;
132        }
133        let min_level = level_from_u8(self.state.level.load(Ordering::Acquire));
134        level_rank(level) >= level_rank(min_level)
135    }
136
137    fn is_metadata_enabled(&self, metadata: &Metadata<'_>) -> bool {
138        let level = tracing_level_to_log_level(metadata.level());
139        level != LogLevel::None && self.is_enabled_for(level)
140    }
141}
142
143impl<S> Layer<S> for XlogLayer
144where
145    S: Subscriber + for<'a> LookupSpan<'a>,
146{
147    fn enabled(&self, metadata: &Metadata<'_>, _ctx: Context<'_, S>) -> bool {
148        self.is_metadata_enabled(metadata)
149    }
150
151    fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
152        let metadata = event.metadata();
153        let level = tracing_level_to_log_level(metadata.level());
154        if level == LogLevel::None {
155            return;
156        }
157        if !self.is_enabled_for(level) {
158            return;
159        }
160        if !self.state.logger.is_enabled(level) {
161            return;
162        }
163
164        let mut visitor = EventVisitor::default();
165        event.record(&mut visitor);
166
167        let mut message = visitor.finish();
168        if self.include_spans {
169            if let Some(scope) = ctx.event_scope(event) {
170                let mut spans = String::new();
171                for span in scope.from_root() {
172                    if !spans.is_empty() {
173                        spans.push_str(" > ");
174                    }
175                    spans.push_str(span.metadata().name());
176                }
177                if !spans.is_empty() {
178                    if message.is_empty() {
179                        message = spans;
180                    } else {
181                        message = format!("[{}] {}", spans, message);
182                    }
183                }
184            }
185        }
186        if message.is_empty() {
187            message = metadata.name().to_string();
188        }
189
190        let tag = self.tag.as_deref().unwrap_or_else(|| metadata.target());
191        let file = metadata.file().unwrap_or("<unknown>");
192        let module = metadata.module_path().unwrap_or("<unknown>");
193        let line = metadata.line().unwrap_or(0);
194
195        self.state
196            .logger
197            .write_with_meta(level, Some(tag), file, module, line, &message);
198    }
199}
200
201struct LayerState {
202    enabled: AtomicBool,
203    level: AtomicU8,
204    logger: Xlog,
205}
206
207impl LayerState {
208    fn new(logger: Xlog, enabled: bool, level: LogLevel) -> Self {
209        Self {
210            enabled: AtomicBool::new(enabled),
211            level: AtomicU8::new(level_to_u8(level)),
212            logger,
213        }
214    }
215}
216
217#[derive(Default)]
218struct EventVisitor {
219    message: Option<String>,
220    fields: Vec<(String, String)>,
221}
222
223impl EventVisitor {
224    fn finish(self) -> String {
225        let mut output = String::new();
226        if let Some(message) = self.message {
227            output.push_str(&message);
228        }
229        if !self.fields.is_empty() {
230            if !output.is_empty() {
231                output.push(' ');
232            }
233            output.push('{');
234            for (idx, (name, value)) in self.fields.iter().enumerate() {
235                if idx > 0 {
236                    output.push_str(", ");
237                }
238                output.push_str(name);
239                output.push('=');
240                output.push_str(value);
241            }
242            output.push('}');
243        }
244        output
245    }
246
247    fn record_field(&mut self, field: &Field, value: String) {
248        if field.name() == "message" {
249            self.message = Some(value);
250        } else {
251            self.fields.push((field.name().to_string(), value));
252        }
253    }
254}
255
256impl Visit for EventVisitor {
257    fn record_f64(&mut self, field: &Field, value: f64) {
258        self.record_field(field, value.to_string());
259    }
260
261    fn record_i64(&mut self, field: &Field, value: i64) {
262        self.record_field(field, value.to_string());
263    }
264
265    fn record_u64(&mut self, field: &Field, value: u64) {
266        self.record_field(field, value.to_string());
267    }
268
269    fn record_bool(&mut self, field: &Field, value: bool) {
270        self.record_field(field, value.to_string());
271    }
272
273    fn record_str(&mut self, field: &Field, value: &str) {
274        self.record_field(field, value.to_string());
275    }
276
277    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
278        self.record_field(field, format!("{value:?}"));
279    }
280}
281
282fn tracing_level_to_log_level(level: &Level) -> LogLevel {
283    match *level {
284        Level::TRACE => LogLevel::Verbose,
285        Level::DEBUG => LogLevel::Debug,
286        Level::INFO => LogLevel::Info,
287        Level::WARN => LogLevel::Warn,
288        Level::ERROR => LogLevel::Error,
289    }
290}
291
292fn level_rank(level: LogLevel) -> u8 {
293    match level {
294        LogLevel::Verbose => 0,
295        LogLevel::Debug => 1,
296        LogLevel::Info => 2,
297        LogLevel::Warn => 3,
298        LogLevel::Error => 4,
299        LogLevel::Fatal => 5,
300        LogLevel::None => 6,
301    }
302}
303
304fn level_to_u8(level: LogLevel) -> u8 {
305    level_rank(level)
306}
307
308fn level_from_u8(value: u8) -> LogLevel {
309    match value {
310        0 => LogLevel::Verbose,
311        1 => LogLevel::Debug,
312        2 => LogLevel::Info,
313        3 => LogLevel::Warn,
314        4 => LogLevel::Error,
315        5 => LogLevel::Fatal,
316        _ => LogLevel::None,
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use std::sync::atomic::{AtomicUsize, Ordering};
323
324    use tempfile::TempDir;
325
326    use super::{XlogLayer, XlogLayerConfig};
327    use crate::{LogLevel, Xlog, XlogConfig};
328
329    static NEXT_PREFIX_ID: AtomicUsize = AtomicUsize::new(1);
330
331    fn unique_prefix() -> String {
332        let id = NEXT_PREFIX_ID.fetch_add(1, Ordering::Relaxed);
333        format!("tracing-layer-{}-{id}", std::process::id())
334    }
335
336    #[test]
337    fn with_config_does_not_mutate_logger_level() {
338        let dir = TempDir::new().expect("tempdir");
339        let logger = Xlog::init(
340            XlogConfig::new(dir.path().display().to_string(), unique_prefix()),
341            LogLevel::Info,
342        )
343        .expect("init logger");
344        assert_eq!(logger.level(), LogLevel::Info);
345
346        let (_layer, _handle) =
347            XlogLayer::with_config(logger.clone(), XlogLayerConfig::new(LogLevel::Debug));
348        assert_eq!(logger.level(), LogLevel::Info);
349    }
350
351    #[test]
352    fn handle_set_level_only_updates_layer_filter() {
353        let dir = TempDir::new().expect("tempdir");
354        let logger = Xlog::init(
355            XlogConfig::new(dir.path().display().to_string(), unique_prefix()),
356            LogLevel::Warn,
357        )
358        .expect("init logger");
359        let (_layer, handle) =
360            XlogLayer::with_config(logger.clone(), XlogLayerConfig::new(LogLevel::Info));
361
362        assert_eq!(logger.level(), LogLevel::Warn);
363        assert_eq!(handle.level(), LogLevel::Info);
364
365        handle.set_level(LogLevel::Debug);
366        assert_eq!(handle.level(), LogLevel::Debug);
367        assert_eq!(logger.level(), LogLevel::Warn);
368    }
369}