collectd_plugin/api/logger.rs
1use crate::bindings::{plugin_log, LOG_DEBUG, LOG_ERR, LOG_INFO, LOG_NOTICE, LOG_WARNING};
2use crate::errors::FfiError;
3use log::{error, log_enabled, Level, LevelFilter, Metadata, Record, SetLoggerError};
4use std::cell::Cell;
5use std::error::Error;
6use std::ffi::{CStr, CString};
7use std::fmt::Write as FmtWrite;
8use std::io::{self, Write};
9
10/// A builder for configuring and installing a collectd logger.
11///
12/// It is recommended to instantiate the logger in `PluginManager::plugins`.
13///
14/// The use case of multiple rust plugins that instantiate a global logger is supported. Each
15/// plugin will have its own copy of a global logger. Thus plugins won't interefere with others and
16/// their logging.
17pub struct CollectdLoggerBuilder {
18 plugin: Option<&'static str>,
19 filter_level: LevelFilter,
20 format: Box<FormatFn>,
21}
22
23type FormatFn = dyn Fn(&mut dyn Write, &Record<'_>) -> io::Result<()> + Sync + Send;
24
25impl CollectdLoggerBuilder {
26 /// Creates a new CollectdLoggerBuilder that will forward log messages to collectd.
27 ///
28 /// By default, accepts all log levels and uses the default format.
29 /// See [`CollectdLoggerBuilder::filter_level`] to filter and [`CollectdLoggerBuilder::format`] for custom formatting.
30 pub fn new() -> Self {
31 Self {
32 plugin: None,
33 filter_level: LevelFilter::Trace,
34 format: Format::default().into_boxed_fn(),
35 }
36 }
37
38 /// Sets a prefix using the plugin manager's name.
39 pub fn prefix_plugin<T: crate::plugins::PluginManager>(mut self) -> Self {
40 self.plugin = Some(T::name());
41 self
42 }
43
44 /// Sets the log level filter - only messages at this level and above will be forwarded to collectd.
45 ///
46 /// # Example
47 ///
48 /// ```ignore
49 /// use collectd_plugin::CollectdLogger;
50 /// use log::LevelFilter;
51 ///
52 /// CollectdLogger::new()
53 /// .prefix("myplugin")
54 /// .filter_level(LevelFilter::Info)
55 /// .try_init()?;
56 ///
57 /// log::debug!("This will be filtered out");
58 /// log::info!("This will go to collectd");
59 /// ```
60 pub fn filter_level(mut self, level: LevelFilter) -> Self {
61 self.filter_level = level;
62 self
63 }
64
65 /// Sets the format function for formatting the log output.
66 ///
67 /// # Example
68 ///
69 /// ```ignore
70 /// use collectd_plugin::CollectdLoggerBuilder;
71 /// use std::io::Write;
72 ///
73 /// CollectdLoggerBuilder::new()
74 /// .prefix("myplugin")
75 /// .format(|buf, record| {
76 /// write!(buf, "[{}] {}", record.level(), record.args())
77 /// })
78 /// .try_init()?;
79 /// ```
80 pub fn format<F>(mut self, format: F) -> Self
81 where
82 F: Fn(&mut dyn Write, &Record<'_>) -> io::Result<()> + Sync + Send + 'static,
83 {
84 self.format = Box::new(format);
85 self
86 }
87
88 /// The returned logger implements the `Log` trait and can be installed
89 /// manually or nested within another logger.
90 pub fn build(self) -> CollectdLogger {
91 CollectdLogger {
92 plugin: self.plugin,
93 filter_level: self.filter_level,
94 format: self.format,
95 }
96 }
97
98 /// Initializes this logger as the global logger.
99 ///
100 /// All log messages will go to collectd via `plugin_log`. This is the only
101 /// appropriate destination for logs in collectd plugins.
102 ///
103 /// # Errors
104 ///
105 /// This function will fail if it is called more than once, or if another
106 /// library has already initialized a global logger.
107 pub fn try_init(self) -> Result<(), SetLoggerError> {
108 let logger = self.build();
109 log::set_max_level(logger.filter_level);
110 log::set_boxed_logger(Box::new(logger))
111 }
112}
113
114#[derive(Default)]
115struct Format {
116 custom_format: Option<Box<FormatFn>>,
117}
118
119impl Format {
120 fn into_boxed_fn(self) -> Box<FormatFn> {
121 if let Some(fmt) = self.custom_format {
122 fmt
123 } else {
124 Box::new(move |buf, record| {
125 if let Some(path) = record.module_path() {
126 write!(buf, "{}: ", path)?;
127 }
128
129 write!(buf, "{}", record.args())
130 })
131 }
132 }
133}
134
135/// The actual logger implementation that sends messages to collectd.
136pub struct CollectdLogger {
137 plugin: Option<&'static str>,
138 filter_level: LevelFilter,
139 format: Box<FormatFn>,
140}
141
142impl log::Log for CollectdLogger {
143 fn enabled(&self, metadata: &Metadata<'_>) -> bool {
144 metadata.level() <= self.filter_level
145 }
146
147 fn log(&self, record: &Record<'_>) {
148 if !self.enabled(record.metadata()) {
149 return;
150 }
151
152 // Log records are written to a thread local storage before being submitted to
153 // collectd. The buffers are cleared afterwards
154 thread_local!(static LOG_BUF: Cell<Vec<u8>> = const { Cell::new(Vec::new()) });
155 LOG_BUF.with(|cell| {
156 // Replaces the cell's contents with the default value, which is an empty vector.
157 // Should be very cheap to move in and out of
158 let mut write_buffer = cell.take();
159 if let Some(plugin) = self.plugin {
160 // writing the formatting to the vec shouldn't fail unless we ran out of
161 // memory, but in that case, we have a host of other problems.
162 let _ = write!(write_buffer, "{}: ", plugin);
163 }
164
165 if (self.format)(&mut write_buffer, record).is_ok() {
166 let lvl = LogLevel::from(record.level());
167
168 // Force a trailing NUL so that we can use fast path
169 write_buffer.push(b'\0');
170 {
171 let cs = unsafe { CStr::from_bytes_with_nul_unchecked(&write_buffer[..]) };
172 unsafe { plugin_log(lvl as i32, cs.as_ptr()) };
173 }
174 }
175
176 write_buffer.clear();
177 cell.set(write_buffer);
178 });
179 }
180
181 fn flush(&self) {}
182}
183
184/// Logs an error with a description and all the causes. If rust's logging mechanism has been
185/// registered, it is the preferred mechanism. If the Rust logging is not configured (and
186/// considering that an error message should be logged) we log it directly to collectd
187pub fn log_err(desc: &str, err: &FfiError<'_>) {
188 let mut msg = format!("{} error: {}", desc, err);
189
190 // We join all the causes into a single string. Some thoughts
191 // - When an error occurs, one should expect there is some performance price to pay
192 // for additional, and much needed, context
193 // - While nearly all languages will display each cause on a separate line for a
194 // stacktrace, I'm not aware of any collectd plugin doing the same. So to keep
195 // convention, all causes are logged on the same line, semicolon delimited.
196 let mut ie = err.source();
197 while let Some(cause) = ie {
198 let _ = write!(msg, "; {}", cause);
199 ie = cause.source();
200 }
201
202 if log_enabled!(Level::Error) {
203 error!("{}", msg);
204 } else {
205 collectd_log(LogLevel::Error, &msg);
206 }
207}
208
209/// Sends message and log level to collectd. This bypasses any configuration setup via
210/// the global logger, so collectd configuration soley determines if a level is logged
211/// and where it is delivered. Messages that are too long are truncated (1024 was the max length as
212/// of collectd-5.7).
213///
214/// In general, prefer using the `log` crate macros with `CollectdLogger`.
215///
216/// # Panics
217///
218/// If a message containing a null character is given as a message this function will panic.
219pub fn collectd_log(lvl: LogLevel, message: &str) {
220 let cs = CString::new(message).expect("Collectd log to not contain nulls");
221 unsafe {
222 // Collectd will allocate another string behind the scenes before passing to plugins that
223 // registered a log hook, so passing it a string slice is fine.
224 plugin_log(lvl as i32, cs.as_ptr());
225 }
226}
227
228/// A simple wrapper around the collectd's plugin_log, which in turn wraps `vsnprintf`.
229///
230/// ```ignore
231/// collectd_log_raw!(LogLevel::Info, b"test %d\0", 10);
232/// ```
233///
234/// Since this is a low level wrapper, prefer using the `log` crate with `CollectdLogger`. The only times you would
235/// prefer `collectd_log_raw`:
236///
237/// - Collectd was not compiled with `COLLECTD_DEBUG` (chances are, your collectd is compiled with
238/// debugging enabled) and you are logging a debug message.
239/// - The performance price of string formatting in rust instead of C is too large (this shouldn't
240/// be the case)
241///
242/// Undefined behavior can result from any of the following:
243///
244/// - If the format string is not null terminated
245/// - If any string arguments are not null terminated. Use `CString::as_ptr()` to ensure null
246/// termination
247/// - Malformed format string or mismatched arguments
248///
249/// This expects an already null terminated byte string. In the future once the byte equivalent of
250/// `concat!` is rfc accepted and implemented, this won't be a requirement. I also wish that we
251/// could statically assert that the format string contained a null byte for the last character,
252/// but alas, I think we've hit brick wall with the current Rust compiler.
253///
254/// For ergonomic reasons, the format string is converted to the appropriate C ffi type from a byte
255/// string, but no other string arguments are afforded this luxury (so make sure you convert them).
256#[macro_export]
257macro_rules! collectd_log_raw {
258 ($lvl:expr, $fmt:expr) => ({
259 let level: $crate::LogLevel = $lvl;
260 let level = level as i32;
261 unsafe {
262 $crate::bindings::plugin_log(level, ($fmt).as_ptr() as *const ::std::os::raw::c_char);
263 }
264 });
265 ($lvl:expr, $fmt:expr, $($arg:expr),*) => ({
266 let level: $crate::LogLevel = $lvl;
267 let level = level as i32;
268 unsafe {
269 $crate::bindings::plugin_log(level, ($fmt).as_ptr() as *const ::std::os::raw::c_char, $($arg)*);
270 }
271 });
272}
273
274/// The available levels that collectd exposes to log messages.
275#[derive(Debug, PartialEq, Eq, Clone, Copy)]
276#[repr(u32)]
277pub enum LogLevel {
278 Error = LOG_ERR,
279 Warning = LOG_WARNING,
280 Notice = LOG_NOTICE,
281 Info = LOG_INFO,
282 Debug = LOG_DEBUG,
283}
284
285impl LogLevel {
286 /// Attempts to convert a u32 representing a collectd logging level into a Rust enum
287 pub fn try_from(s: u32) -> Option<LogLevel> {
288 match s {
289 LOG_ERR => Some(LogLevel::Error),
290 LOG_WARNING => Some(LogLevel::Warning),
291 LOG_NOTICE => Some(LogLevel::Notice),
292 LOG_INFO => Some(LogLevel::Info),
293 LOG_DEBUG => Some(LogLevel::Debug),
294 _ => None,
295 }
296 }
297}
298
299impl From<Level> for LogLevel {
300 fn from(lvl: Level) -> Self {
301 match lvl {
302 Level::Error => LogLevel::Error,
303 Level::Warn => LogLevel::Warning,
304 Level::Info => LogLevel::Info,
305 Level::Debug | Level::Trace => LogLevel::Debug,
306 }
307 }
308}