glib/
bridged_logging.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use crate::{gstr, log as glib_log, log_structured_array, translate::*, LogField, LogWriterOutput};
4
5// rustdoc-stripper-ignore-next
6/// Enumeration of the possible formatting behaviours for a
7/// [`GlibLogger`](struct.GlibLogger.html).
8///
9/// In order to use this type, `glib` must be built with the `log` feature
10/// enabled.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum GlibLoggerFormat {
13    // rustdoc-stripper-ignore-next
14    /// A simple format, writing only the message on output.
15    Plain,
16    // rustdoc-stripper-ignore-next
17    /// A simple format, writing file, line and message on output.
18    LineAndFile,
19    // rustdoc-stripper-ignore-next
20    /// A logger using glib structured logging.
21    Structured,
22}
23
24// rustdoc-stripper-ignore-next
25/// Enumeration of the possible domain handling behaviours for a
26/// [`GlibLogger`](struct.GlibLogger.html).
27///
28/// In order to use this type, `glib` must be built with the `log` feature
29/// enabled.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum GlibLoggerDomain {
32    // rustdoc-stripper-ignore-next
33    /// Logs will have no domain specified.
34    None,
35    // rustdoc-stripper-ignore-next
36    /// Logs will use the `target` of the log crate as a domain; this allows
37    /// Rust code, like `warn!(target: "my-domain", "...");` to log to the glib
38    /// logger using the specified domain.
39    CrateTarget,
40    // rustdoc-stripper-ignore-next
41    /// Logs will use the crate path as the log domain.
42    CratePath,
43}
44
45// rustdoc-stripper-ignore-next
46/// An implementation of a [`log`](https://crates.io/crates/log) compatible
47/// logger which logs over glib logging facilities.
48///
49/// In order to use this type, `glib` must be built with the `log` feature
50/// enabled.
51///
52/// Use this if you want to use glib as the main logging output in your application,
53/// and want to route all logging happening through the log crate to glib logging.
54/// If you want the opposite, see
55/// [`rust_log_handler`](fn.rust_log_handler.html).
56///
57/// NOTE: This should never be used when
58/// [`rust_log_handler`](fn.rust_log_handler.html) has
59/// been registered as a default glib log handler, otherwise a stack overflow
60/// will occur.
61///
62/// Example:
63///
64/// ```no_compile
65/// static glib_logger: glib::GlibLogger = glib::GlibLogger::new(
66///     glib::GlibLoggerFormat::Plain,
67///     glib::GlibLoggerDomain::CrateTarget,
68/// );
69///
70/// log::set_logger(&glib_logger);
71/// log::set_max_level(log::LevelFilter::Debug);
72///
73/// log::info!("This line will get logged by glib");
74/// ```
75#[derive(Debug)]
76pub struct GlibLogger {
77    format: GlibLoggerFormat,
78    domain: GlibLoggerDomain,
79}
80
81impl GlibLogger {
82    // rustdoc-stripper-ignore-next
83    /// Creates a new instance of [`GlibLogger`](struct.GlibLogger.html).
84    /// See documentation of [`GlibLogger`](struct.GlibLogger.html) for more
85    /// information.
86    ///
87    /// Example:
88    ///
89    /// ```no_compile
90    /// static glib_logger: glib::GlibLogger = glib::GlibLogger::new(
91    ///     glib::GlibLoggerFormat::Plain,
92    ///     glib::GlibLoggerDomain::CrateTarget,
93    /// );
94    ///
95    /// log::set_logger(&glib_logger);
96    /// log::set_max_level(log::LevelFilter::Debug);
97    ///
98    /// log::info!("This line will get logged by glib");
99    /// ```
100    pub const fn new(format: GlibLoggerFormat, domain: GlibLoggerDomain) -> Self {
101        Self { format, domain }
102    }
103
104    fn level_to_glib(level: rs_log::Level) -> crate::LogLevel {
105        match level {
106            // Errors are mapped to critical to avoid automatic termination
107            rs_log::Level::Error => crate::LogLevel::Critical,
108            rs_log::Level::Warn => crate::LogLevel::Warning,
109            rs_log::Level::Info => crate::LogLevel::Info,
110            rs_log::Level::Debug => crate::LogLevel::Debug,
111            // There is no equivalent to trace level in glib
112            rs_log::Level::Trace => crate::LogLevel::Debug,
113        }
114    }
115
116    #[doc(alias = "g_log")]
117    fn write_log(domain: Option<&str>, level: rs_log::Level, message: &std::fmt::Arguments<'_>) {
118        unsafe {
119            use std::fmt::Write;
120
121            let mut message_builder = crate::GStringBuilder::default();
122            if write!(&mut message_builder, "{}", message).is_err() {
123                return;
124            }
125            let message = message_builder.into_string();
126
127            crate::ffi::g_log(
128                domain.to_glib_none().0,
129                GlibLogger::level_to_glib(level).into_glib(),
130                b"%s\0".as_ptr() as *const _,
131                ToGlibPtr::<*const std::os::raw::c_char>::to_glib_none(&message).0,
132            );
133        }
134    }
135
136    fn write_log_structured(
137        domain: Option<&str>,
138        level: rs_log::Level,
139        file: Option<&str>,
140        line: Option<u32>,
141        func: Option<&str>,
142        message: &str,
143    ) {
144        // Write line number into a static array to avoid allocating its string
145        // representation. 16 bytes allow 10^15 lines, which should be more than
146        // sufficient.
147        let mut line_buffer = [0u8; 16];
148        let line = {
149            use std::io::{Cursor, Write};
150            let mut c = Cursor::new(line_buffer.as_mut_slice());
151            match line {
152                Some(lineno) => write!(&mut c, "{lineno}").ok(),
153                None => write!(&mut c, "<unknown line>").ok(),
154            };
155            let pos = c.position() as usize;
156            &line_buffer[..pos]
157        };
158        let glib_level = GlibLogger::level_to_glib(level);
159        let fields = [
160            LogField::new(gstr!("PRIORITY"), glib_level.priority().as_bytes()),
161            LogField::new(
162                gstr!("CODE_FILE"),
163                file.unwrap_or("<unknown file>").as_bytes(),
164            ),
165            LogField::new(gstr!("CODE_LINE"), line),
166            LogField::new(
167                gstr!("CODE_FUNC"),
168                func.unwrap_or("<unknown module path>").as_bytes(),
169            ),
170            LogField::new(gstr!("MESSAGE"), message.as_bytes()),
171            LogField::new(gstr!("GLIB_DOMAIN"), domain.unwrap_or("default").as_bytes()),
172        ];
173        log_structured_array(glib_level, &fields);
174    }
175}
176
177impl rs_log::Log for GlibLogger {
178    fn enabled(&self, _: &rs_log::Metadata) -> bool {
179        true
180    }
181
182    fn log(&self, record: &rs_log::Record) {
183        if !self.enabled(record.metadata()) {
184            return;
185        }
186
187        let domain = match &self.domain {
188            GlibLoggerDomain::None => None,
189            GlibLoggerDomain::CrateTarget => Some(record.metadata().target()),
190            GlibLoggerDomain::CratePath => record.module_path(),
191        };
192
193        match self.format {
194            GlibLoggerFormat::Plain => {
195                GlibLogger::write_log(domain, record.level(), record.args());
196            }
197            GlibLoggerFormat::LineAndFile => {
198                match (record.file(), record.line()) {
199                    (Some(file), Some(line)) => {
200                        GlibLogger::write_log(
201                            domain,
202                            record.level(),
203                            &format_args!("{}:{}: {}", file, line, record.args()),
204                        );
205                    }
206                    (Some(file), None) => {
207                        GlibLogger::write_log(
208                            domain,
209                            record.level(),
210                            &format_args!("{}: {}", file, record.args()),
211                        );
212                    }
213                    _ => {
214                        GlibLogger::write_log(domain, record.level(), record.args());
215                    }
216                };
217            }
218            GlibLoggerFormat::Structured => {
219                let args = record.args();
220                let message = if let Some(s) = args.as_str() {
221                    s
222                } else {
223                    &args.to_string()
224                };
225                GlibLogger::write_log_structured(
226                    domain,
227                    record.level(),
228                    record.file(),
229                    record.line(),
230                    record.module_path(),
231                    message,
232                );
233            }
234        };
235    }
236
237    fn flush(&self) {}
238}
239
240// rustdoc-stripper-ignore-next
241/// Provides a glib log handler which routes all logging messages to the
242/// [`log crate`](https://crates.io/crates/log).
243///
244/// In order to use this function, `glib` must be built with the `log` feature
245/// enabled.
246///
247/// Use this function if you want to use the log crate as the main logging
248/// output in your application, and want to route all logging happening in
249/// glib to the log crate. If you want the opposite, use [`GlibLogger`](struct.GlibLogger.html).
250///
251/// NOTE: This should never be used when [`GlibLogger`](struct.GlibLogger.html) is
252/// registered as a logger, otherwise a stack overflow will occur.
253///
254/// ```no_run
255/// glib::log_set_default_handler(glib::rust_log_handler);
256/// ```
257pub fn rust_log_handler(domain: Option<&str>, level: glib_log::LogLevel, message: &str) {
258    let level = match level {
259        glib_log::LogLevel::Error | glib_log::LogLevel::Critical => rs_log::Level::Error,
260        glib_log::LogLevel::Warning => rs_log::Level::Warn,
261        glib_log::LogLevel::Message | glib_log::LogLevel::Info => rs_log::Level::Info,
262        glib_log::LogLevel::Debug => rs_log::Level::Debug,
263    };
264
265    rs_log::log!(target: domain.unwrap_or_default(), level, "{message}");
266}
267
268#[cfg(feature = "log_kv")]
269struct LogFields<'field>(&'field [LogField<'field>]);
270
271#[cfg(feature = "log_kv")]
272impl<'fields> rs_log::kv::Source for LogFields<'fields> {
273    fn visit<'kvs>(
274        &'kvs self,
275        visitor: &mut dyn rs_log::kv::VisitSource<'kvs>,
276    ) -> Result<(), rs_log::kv::Error> {
277        use rs_log::kv::{Key, Value};
278
279        for field in self.0 {
280            let key = Key::from_str(field.key());
281            let value = field
282                .value_str()
283                .map(Value::from)
284                .unwrap_or_else(Value::null);
285            visitor.visit_pair(key, value)?;
286        }
287
288        Ok(())
289    }
290
291    fn count(&self) -> usize {
292        self.0.len()
293    }
294}
295
296// rustdoc-stripper-ignore-next
297/// Provides a glib log writer which routes all structured logging messages to the
298/// [`log crate`](https://crates.io/crates/log).
299///
300/// In order to use this function, `glib` must be built with the `log` feature
301/// enabled.
302///
303/// Use this function if you want to use the log crate as the main logging
304/// output in your application, and want to route all structured logging happening in
305/// glib to the log crate. If you want the opposite, use [`GlibLogger`](struct.GlibLogger.html).
306///
307/// NOTE: This should never be used when [`GlibLogger`](struct.GlibLogger.html) is
308/// registered as a logger, otherwise a stack overflow will occur.
309///
310/// ```no_run
311/// glib::log_set_writer_func(glib::rust_log_writer);
312/// ```
313pub fn rust_log_writer(log_level: glib_log::LogLevel, fields: &[LogField<'_>]) -> LogWriterOutput {
314    let lvl = match log_level {
315        glib_log::LogLevel::Error | glib_log::LogLevel::Critical => rs_log::Level::Error,
316        glib_log::LogLevel::Warning => rs_log::Level::Warn,
317        glib_log::LogLevel::Info | glib_log::LogLevel::Message => rs_log::Level::Info,
318        glib_log::LogLevel::Debug => rs_log::Level::Debug,
319    };
320
321    if lvl > rs_log::STATIC_MAX_LEVEL {
322        return LogWriterOutput::Handled;
323    }
324
325    let mut domain = None::<&str>;
326    let mut message = None::<&str>;
327    let mut file = None::<&str>;
328    let mut line = None::<u32>;
329    let mut func = None::<&str>;
330
331    for field in fields {
332        let Some(value) = field.value_str() else {
333            continue;
334        };
335
336        match field.key() {
337            "GLIB_DOMAIN" => domain = Some(value),
338            "MESSAGE" => message = Some(value),
339            "CODE_FILE" => file = Some(value),
340            "CODE_LINE" => line = value.parse().ok(),
341            "CODE_FUNC" => func = Some(value),
342            _ => continue,
343        };
344    }
345
346    if let Some(message) = message {
347        let logger = rs_log::logger();
348        let mut record = rs_log::Record::builder();
349
350        #[cfg(feature = "log_kv")]
351        let fields = LogFields(fields);
352
353        record
354            .level(lvl)
355            .target(domain.unwrap_or_default())
356            .file(file)
357            .line(line)
358            .module_path(func);
359
360        #[cfg(feature = "log_kv")]
361        record.key_values(&fields);
362
363        logger.log(&record.args(format_args!("{message}")).build());
364    }
365
366    LogWriterOutput::Handled
367}
368
369// rustdoc-stripper-ignore-next
370/// A macro which behaves exactly as `log::error!` except that it sets the
371/// current log target to the contents of a `G_LOG_DOMAIN` constant (and fails
372/// to build if not defined).
373///
374/// In order to use this macro, `glib` must be built with the `log_macros`
375/// feature enabled and the [`GlibLogger`](struct.GlibLogger.html) must have been
376/// initialized using [`GlibLoggerDomain::CrateTarget`](enum.GlibLoggerDomain.html).
377///
378/// ```no_run
379/// static G_LOG_DOMAIN: &str = "my-domain";
380///
381/// glib::error!("This will be logged under 'my-domain'");
382/// ```
383#[macro_export]
384#[cfg(any(docsrs, feature = "log_macros"))]
385#[cfg_attr(docsrs, doc(cfg(feature = "log_macros")))]
386macro_rules! error {
387    (target: $target:expr, $($arg:tt)+) => (
388        $crate::rs_log::log!(target: $target, $crate::rs_log::Level::Error, $($arg)+);
389    );
390    ($($arg:tt)+) => (
391        $crate::rs_log::log!(target: G_LOG_DOMAIN, $crate::rs_log::Level::Error, $($arg)+);
392    )
393}
394
395// rustdoc-stripper-ignore-next
396/// A macro which behaves exactly as `log::warn!` except that it sets the
397/// current log target to the contents of a `G_LOG_DOMAIN` constant (and fails
398/// to build if not defined).
399///
400/// In order to use this macro, `glib` must be built with the `log_macros`
401/// feature enabled and the [`GlibLogger`](struct.GlibLogger.html) must have been
402/// initialized using [`GlibLoggerDomain::CrateTarget`](enum.GlibLoggerDomain.html).
403///
404/// ```no_run
405/// static G_LOG_DOMAIN: &str = "my-domain";
406///
407/// glib::warn!("This will be logged under 'my-domain'");
408/// ```
409#[macro_export]
410#[cfg(any(docsrs, feature = "log_macros"))]
411#[cfg_attr(docsrs, doc(cfg(feature = "log_macros")))]
412macro_rules! warn {
413    (target: $target:expr, $($arg:tt)+) => (
414        $crate::rs_log::log!(target: $target, $crate::rs_log::Level::Warn, $($arg)+);
415    );
416    ($($arg:tt)+) => (
417        $crate::rs_log::log!(target: G_LOG_DOMAIN, $crate::rs_log::Level::Warn, $($arg)+);
418    )
419}
420
421// rustdoc-stripper-ignore-next
422/// A macro which behaves exactly as `log::info!` except that it sets the
423/// current log target to the contents of a `G_LOG_DOMAIN` constant (and fails
424/// to build if not defined).
425///
426/// In order to use this macro, `glib` must be built with the `log_macros`
427/// feature enabled and the [`GlibLogger`](struct.GlibLogger.html) must have been
428/// initialized using [`GlibLoggerDomain::CrateTarget`](enum.GlibLoggerDomain.html).
429///
430/// ```no_run
431/// static G_LOG_DOMAIN: &str = "my-domain";
432///
433/// glib::info!("This will be logged under 'my-domain'");
434/// ```
435#[macro_export]
436#[cfg(any(docsrs, feature = "log_macros"))]
437#[cfg_attr(docsrs, doc(cfg(feature = "log_macros")))]
438macro_rules! info {
439    (target: $target:expr, $($arg:tt)+) => (
440        $crate::rs_log::log!(target: $target, $crate::rs_log::Level::Info, $($arg)+);
441    );
442    ($($arg:tt)+) => (
443        $crate::rs_log::log!(target: G_LOG_DOMAIN, $crate::rs_log::Level::Info, $($arg)+);
444    )
445}
446
447// rustdoc-stripper-ignore-next
448/// A macro which behaves exactly as `log::debug!` except that it sets the
449/// current log target to the contents of a `G_LOG_DOMAIN` constant (and fails
450/// to build if not defined).
451///
452/// In order to use this macro, `glib` must be built with the `log_macros`
453/// feature enabled and the [`GlibLogger`](struct.GlibLogger.html) must have been
454/// initialized using [`GlibLoggerDomain::CrateTarget`](enum.GlibLoggerDomain.html).
455///
456/// ```no_run
457/// static G_LOG_DOMAIN: &str = "my-domain";
458///
459/// glib::debug!("This will be logged under 'my-domain'");
460/// ```
461#[macro_export]
462#[cfg(any(docsrs, feature = "log_macros"))]
463#[cfg_attr(docsrs, doc(cfg(feature = "log_macros")))]
464macro_rules! debug {
465    (target: $target:expr, $($arg:tt)+) => (
466        $crate::rs_log::log!(target: $target, $crate::rs_log::Level::Debug, $($arg)+);
467    );
468    ($($arg:tt)+) => (
469        $crate::rs_log::log!(target: G_LOG_DOMAIN, $crate::rs_log::Level::Debug, $($arg)+);
470    )
471}
472
473// rustdoc-stripper-ignore-next
474/// A macro which behaves exactly as `log::trace!` except that it sets the
475/// current log target to the contents of a `G_LOG_DOMAIN` constant (and fails
476/// to build if not defined).
477///
478/// In order to use this macro, `glib` must be built with the `log_macros`
479/// feature enabled and the [`GlibLogger`](struct.GlibLogger.html) must have been
480/// initialized using [`GlibLoggerDomain::CrateTarget`](enum.GlibLoggerDomain.html).
481///
482/// ```no_run
483/// static G_LOG_DOMAIN: &str = "my-domain";
484///
485/// glib::trace!("This will be logged under 'my-domain'");
486/// ```
487#[macro_export]
488#[cfg(any(docsrs, feature = "log_macros"))]
489#[cfg_attr(docsrs, doc(cfg(feature = "log_macros")))]
490macro_rules! trace {
491    (target: $target:expr, $($arg:tt)+) => (
492        $crate::rs_log::log!(target: $target, $crate::rs_log::Level::Trace, $($arg)+);
493    );
494    ($($arg:tt)+) => (
495        $crate::rs_log::log!(target: G_LOG_DOMAIN, $crate::rs_log::Level::Trace, $($arg)+);
496    )
497}