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 and fields from the event
61        let mut visitor = MessageVisitor::default();
62        event.record(&mut visitor);
63
64        let message = visitor.into_message();
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 and format all fields from tracing events
78#[derive(Default)]
79struct MessageVisitor {
80    message: Option<String>,
81    fields: Vec<(String, String)>,
82}
83
84impl Visit for MessageVisitor {
85    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
86        let name = field.name();
87        if name == "message" {
88            self.message = Some(format!("{:?}", value));
89        } else {
90            // Collect all other fields as key=value pairs
91            self.fields.push((name.to_string(), format!("{:?}", value)));
92        }
93    }
94
95    fn record_str(&mut self, field: &Field, value: &str) {
96        let name = field.name();
97        if name == "message" {
98            self.message = Some(value.to_string());
99        } else {
100            // Collect all other fields as key=value pairs
101            self.fields.push((name.to_string(), value.to_string()));
102        }
103    }
104
105    fn record_i64(&mut self, field: &Field, value: i64) {
106        let name = field.name();
107        if name != "message" {
108            self.fields.push((name.to_string(), value.to_string()));
109        }
110    }
111
112    fn record_u64(&mut self, field: &Field, value: u64) {
113        let name = field.name();
114        if name != "message" {
115            self.fields.push((name.to_string(), value.to_string()));
116        }
117    }
118
119    fn record_bool(&mut self, field: &Field, value: bool) {
120        let name = field.name();
121        if name != "message" {
122            self.fields.push((name.to_string(), value.to_string()));
123        }
124    }
125}
126
127impl MessageVisitor {
128    /// Build the final formatted message with all fields
129    fn into_message(self) -> String {
130        let mut result = self.message.unwrap_or_default();
131
132        // Append structured fields as key=value pairs
133        if !self.fields.is_empty() {
134            if !result.is_empty() {
135                result.push(' ');
136            }
137            for (i, (key, value)) in self.fields.iter().enumerate() {
138                if i > 0 {
139                    result.push(' ');
140                }
141                result.push_str(&format!("{}={}", key, value));
142            }
143        }
144
145        result
146    }
147}
148
149/// Initialize the logging system with the FFI layer
150///
151/// This sets up tracing with the FFI logging layer. Call this once during
152/// plugin initialization. Subsequent calls after the first initialization
153/// are no-ops since the subscriber is global.
154pub fn init_logging() {
155    use once_cell::sync::OnceCell;
156    use tracing_subscriber::filter::LevelFilter;
157    use tracing_subscriber::prelude::*;
158    use tracing_subscriber::reload;
159
160    // Use OnceCell to ensure we only initialize once
161    static INITIALIZED: OnceCell<()> = OnceCell::new();
162
163    INITIALIZED.get_or_init(|| {
164        let layer = FfiLoggingLayer::new();
165
166        // Create a reloadable level filter
167        let initial_level = LogCallbackManager::global().level();
168        let initial_filter = match initial_level {
169            LogLevel::Trace => LevelFilter::TRACE,
170            LogLevel::Debug => LevelFilter::DEBUG,
171            LogLevel::Info => LevelFilter::INFO,
172            LogLevel::Warn => LevelFilter::WARN,
173            LogLevel::Error => LevelFilter::ERROR,
174            LogLevel::Off => LevelFilter::OFF,
175        };
176
177        let (filter, reload_handle) = reload::Layer::new(initial_filter);
178
179        // Store the reload handle for later use
180        crate::reload::ReloadHandle::global().set_handle(reload_handle);
181
182        // Create subscriber with reloadable filter first, then FFI layer
183        let subscriber = tracing_subscriber::registry().with(filter).with(layer);
184
185        // Set as global default - ignore error if already set
186        let _ = tracing::subscriber::set_global_default(subscriber);
187    });
188}
189
190/// Initialize logging with a specific log level
191#[allow(dead_code)] // Public API for plugin authors
192pub fn init_logging_with_level(level: LogLevel) {
193    LogCallbackManager::global().set_level(level);
194    init_logging();
195}
196
197#[cfg(test)]
198#[path = "layer/layer_tests.rs"]
199mod layer_tests;