Skip to main content

haproxy_spoa_hub_plugin_api/
logging.rs

1//! Forwarding of a plugin's `log` records to the hub.
2//!
3//! A plugin `.so` carries its own copy of the `log` crate, so a logger
4//! the hub installs in its own process image is invisible to the plugin
5//! and its records go nowhere. The hub therefore hands each plugin a
6//! [`LogSinkFn`] callback (v4 vtable field `set_log_sink`, called once
7//! between `create` and `init`); [`HubLogSink`] is a `log::Log`
8//! implementation, installed as the plugin's global logger by the
9//! `define_plugin!` macro, that forwards every enabled record through
10//! that callback. Plugin authors only use the `log` macros and must not
11//! install a logger: the hub's is in place before `init` runs, so a
12//! `log::set_logger` there fails (and an unwrapping `env_logger::init()`
13//! fails the plugin load).
14//!
15//! # Lifetime contract
16//!
17//! Same as the metric recorder: the sink callback and its `ctx` remain
18//! valid from `set_log_sink` until the plugin's `destroy` returns; the
19//! hub keys `ctx` by plugin name and never frees it, so a record emitted
20//! from a background thread after a reload still lands.
21
22use std::os::raw::c_void;
23use std::sync::RwLock;
24
25use abi_stable::std_types::RStr;
26
27/// Severity of a forwarded record. Discriminants equal `log::Level`'s
28/// (`Error = 1` … `Trace = 5`); `Off` only appears as a maximum level.
29#[repr(u8)]
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum LogLevel {
32    Off = 0,
33    Error = 1,
34    Warn = 2,
35    Info = 3,
36    Debug = 4,
37    Trace = 5,
38}
39
40impl From<log::Level> for LogLevel {
41    fn from(level: log::Level) -> Self {
42        match level {
43            log::Level::Error => LogLevel::Error,
44            log::Level::Warn => LogLevel::Warn,
45            log::Level::Info => LogLevel::Info,
46            log::Level::Debug => LogLevel::Debug,
47            log::Level::Trace => LogLevel::Trace,
48        }
49    }
50}
51
52impl From<LogLevel> for log::LevelFilter {
53    fn from(level: LogLevel) -> Self {
54        match level {
55            LogLevel::Off => log::LevelFilter::Off,
56            LogLevel::Error => log::LevelFilter::Error,
57            LogLevel::Warn => log::LevelFilter::Warn,
58            LogLevel::Info => log::LevelFilter::Info,
59            LogLevel::Debug => log::LevelFilter::Debug,
60            LogLevel::Trace => log::LevelFilter::Trace,
61        }
62    }
63}
64
65/// FFI signature the hub provides to each plugin via
66/// [`PluginVTable::set_log_sink`]. `ctx` is opaque and hub-owned;
67/// `target` is the record's `log` target (the plugin module path);
68/// `message` is the formatted record.
69///
70/// [`PluginVTable::set_log_sink`]: crate::PluginVTable::set_log_sink
71pub type LogSinkFn =
72    extern "C" fn(ctx: *const c_void, level: LogLevel, target: RStr<'_>, message: RStr<'_>);
73
74/// The plugin-side `log::Log` implementation that forwards to the hub.
75/// One per `.so`; two plugin instances loaded from the same library
76/// share it, and records are attributed to whichever instance installed
77/// the sink last.
78pub struct HubLogSink {
79    inner: RwLock<Option<SinkInner>>,
80}
81
82struct SinkInner {
83    sink_fn: LogSinkFn,
84    ctx: *const c_void,
85}
86
87// SAFETY: `ctx` points into hub-owned memory that the hub keeps valid and
88// addressable from any thread for the plugin's lifetime, and the sink
89// function is thread-safe (it dispatches into `tracing`). Both fields are
90// read-only after installation.
91unsafe impl Send for SinkInner {}
92unsafe impl Sync for SinkInner {}
93
94/// The sink `define_plugin!` installs as the plugin's global logger.
95pub static HUB_LOG_SINK: HubLogSink = HubLogSink::new();
96
97impl HubLogSink {
98    const fn new() -> Self {
99        Self {
100            inner: RwLock::new(None),
101        }
102    }
103
104    /// Install (or replace, on a plugin reload) the hub's sink and make
105    /// this the plugin's global logger. `max_level` is the most verbose
106    /// level the hub will emit, so cheaper records are skipped before
107    /// they are formatted.
108    #[doc(hidden)]
109    pub fn install(&self, sink_fn: LogSinkFn, ctx: *const c_void, max_level: LogLevel) {
110        let new = SinkInner { sink_fn, ctx };
111        match self.inner.write() {
112            Ok(mut guard) => *guard = Some(new),
113            Err(poisoned) => *poisoned.into_inner() = Some(new),
114        }
115        // Err means a logger is already set — ours from an earlier plugin
116        // generation of this library; it reads the replaced inner.
117        let _ = log::set_logger(&HUB_LOG_SINK);
118        log::set_max_level(max_level.into());
119    }
120}
121
122impl log::Log for HubLogSink {
123    fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
124        metadata.level() <= log::max_level()
125    }
126
127    fn log(&self, record: &log::Record<'_>) {
128        if !self.enabled(record.metadata()) {
129            return;
130        }
131        // Held across the callback so a racing reload's `install` cannot
132        // swap the inner mid-call.
133        let guard = match self.inner.read() {
134            Ok(g) => g,
135            Err(poisoned) => poisoned.into_inner(),
136        };
137        let Some(ref inner) = *guard else {
138            return;
139        };
140        let message = record.args().to_string();
141        (inner.sink_fn)(
142            inner.ctx,
143            record.level().into(),
144            RStr::from(record.target()),
145            RStr::from(message.as_str()),
146        );
147    }
148
149    fn flush(&self) {}
150}