Skip to main content

rustbridge_logging/
layer.rs

1//! Tracing layer that forwards to FFI callbacks
2
3use crate::callback::LogCallbackManager;
4use rustbridge_core::LogLevel;
5use tracing::field::{Field, Visit};
6use tracing::{Event, Level, Subscriber};
7use tracing_subscriber::Layer;
8use tracing_subscriber::layer::Context;
9use tracing_subscriber::registry::LookupSpan;
10
11/// Tracing layer that forwards log events to FFI callbacks
12pub struct FfiLoggingLayer {
13    manager: &'static LogCallbackManager,
14}
15
16impl FfiLoggingLayer {
17    /// Create a new FFI logging layer using the global callback manager
18    pub fn new() -> Self {
19        Self {
20            manager: LogCallbackManager::global(),
21        }
22    }
23
24    /// Create a layer with a specific callback manager
25    pub fn with_manager(manager: &'static LogCallbackManager) -> Self {
26        Self { manager }
27    }
28
29    /// Convert tracing Level to our LogLevel
30    fn convert_level(level: &Level) -> LogLevel {
31        match *level {
32            Level::TRACE => LogLevel::Trace,
33            Level::DEBUG => LogLevel::Debug,
34            Level::INFO => LogLevel::Info,
35            Level::WARN => LogLevel::Warn,
36            Level::ERROR => LogLevel::Error,
37        }
38    }
39}
40
41impl Default for FfiLoggingLayer {
42    fn default() -> Self {
43        Self::new()
44    }
45}
46
47impl<S> Layer<S> for FfiLoggingLayer
48where
49    S: Subscriber + for<'a> LookupSpan<'a>,
50{
51    fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
52        let metadata = event.metadata();
53        let level = Self::convert_level(metadata.level());
54
55        // Check if this level is enabled before doing any work
56        if !self.manager.is_enabled(level) {
57            return;
58        }
59
60        // Extract the message from the event
61        let mut visitor = MessageVisitor::default();
62        event.record(&mut visitor);
63
64        let message = visitor.message.unwrap_or_default();
65        let target = metadata.target();
66
67        // Forward to the callback
68        self.manager.log(level, target, &message);
69    }
70
71    fn enabled(&self, metadata: &tracing::Metadata<'_>, _ctx: Context<'_, S>) -> bool {
72        let level = Self::convert_level(metadata.level());
73        self.manager.is_enabled(level)
74    }
75}
76
77/// Visitor to extract the message field from tracing events
78#[derive(Default)]
79struct MessageVisitor {
80    message: Option<String>,
81}
82
83impl Visit for MessageVisitor {
84    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
85        if field.name() == "message" {
86            self.message = Some(format!("{:?}", value));
87        } else if self.message.is_none() {
88            // If no "message" field, use the first field
89            self.message = Some(format!("{:?}", value));
90        }
91    }
92
93    fn record_str(&mut self, field: &Field, value: &str) {
94        // Use "message" field if present, otherwise use first field encountered
95        if field.name() == "message" || self.message.is_none() {
96            self.message = Some(value.to_string());
97        }
98    }
99}
100
101/// Initialize the logging system with the FFI layer
102///
103/// This sets up tracing with the FFI logging layer. Call this once during
104/// plugin initialization. Subsequent calls after the first initialization
105/// are no-ops since the subscriber is global.
106pub fn init_logging() {
107    use once_cell::sync::OnceCell;
108    use tracing_subscriber::filter::LevelFilter;
109    use tracing_subscriber::prelude::*;
110    use tracing_subscriber::reload;
111
112    // Use OnceCell to ensure we only initialize once
113    static INITIALIZED: OnceCell<()> = OnceCell::new();
114
115    INITIALIZED.get_or_init(|| {
116        let layer = FfiLoggingLayer::new();
117
118        // Create a reloadable level filter
119        let initial_level = LogCallbackManager::global().level();
120        let initial_filter = match initial_level {
121            LogLevel::Trace => LevelFilter::TRACE,
122            LogLevel::Debug => LevelFilter::DEBUG,
123            LogLevel::Info => LevelFilter::INFO,
124            LogLevel::Warn => LevelFilter::WARN,
125            LogLevel::Error => LevelFilter::ERROR,
126            LogLevel::Off => LevelFilter::OFF,
127        };
128
129        let (filter, reload_handle) = reload::Layer::new(initial_filter);
130
131        // Store the reload handle for later use
132        crate::reload::ReloadHandle::global().set_handle(reload_handle);
133
134        // Create subscriber with reloadable filter first, then FFI layer
135        let subscriber = tracing_subscriber::registry().with(filter).with(layer);
136
137        // Set as global default - ignore error if already set
138        let _ = tracing::subscriber::set_global_default(subscriber);
139    });
140}
141
142/// Initialize logging with a specific log level
143#[allow(dead_code)] // Public API for plugin authors
144pub fn init_logging_with_level(level: LogLevel) {
145    LogCallbackManager::global().set_level(level);
146    init_logging();
147}
148
149#[cfg(test)]
150#[path = "layer/layer_tests.rs"]
151mod layer_tests;