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}