spdlog/sink/
journald_sink.rs

1use std::{io, os::raw::c_int};
2
3use crate::{
4    formatter::{Formatter, FormatterContext, JournaldFormatter},
5    sink::{GetSinkProp, Sink, SinkProp},
6    Error, ErrorHandler, Level, LevelFilter, Record, Result, StdResult, StringBuf,
7};
8
9#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
10enum SyslogLevel {
11    _Emerg = 0,
12    _Alert = 1,
13    Crit = 2,
14    Err = 3,
15    Warning = 4,
16    _Notice = 5,
17    Info = 6,
18    Debug = 7,
19}
20
21#[derive(Clone, Eq, PartialEq, Hash, Debug)]
22struct SyslogLevels([SyslogLevel; Level::count()]);
23
24impl SyslogLevels {
25    #[must_use]
26    const fn new() -> Self {
27        Self([
28            SyslogLevel::Crit,    // Critical
29            SyslogLevel::Err,     // Error
30            SyslogLevel::Warning, // Warn
31            SyslogLevel::Info,    // Info
32            SyslogLevel::Debug,   // Debug
33            SyslogLevel::Debug,   // Trace
34        ])
35    }
36
37    #[must_use]
38    fn level(&self, level: Level) -> SyslogLevel {
39        self.0[level as usize]
40    }
41}
42
43impl Default for SyslogLevels {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49fn journal_send(args: impl Iterator<Item = impl AsRef<str>>) -> StdResult<(), io::Error> {
50    #[cfg(not(doc))] // https://github.com/rust-lang/rust/issues/97976
51    use libsystemd_sys::{const_iovec, journal as ffi};
52
53    let iovecs: Vec<_> = args.map(|a| unsafe { const_iovec::from_str(a) }).collect();
54    let result = unsafe { ffi::sd_journal_sendv(iovecs.as_ptr(), iovecs.len() as c_int) };
55    if result == 0 {
56        Ok(())
57    } else {
58        Err(io::Error::from_raw_os_error(result))
59    }
60}
61
62/// A sink with systemd-journal as the target.
63///
64/// # Log Level Mapping
65///
66/// | spdlog-rs  | journald  |
67/// |------------|-----------|
68/// | `Critical` | `crit`    |
69/// | `Error`    | `err`     |
70/// | `Warn`     | `warning` |
71/// | `Info`     | `info`    |
72/// | `Debug`    | `debug`   |
73/// | `Trace`    | `debug`   |
74///
75/// # Note
76///
77/// It requires an additional system dependency `libsystemd`.
78///
79/// ## Install on Ubuntu / Debian
80///
81/// ```bash
82/// apt install libsystemd-dev
83/// ```
84///
85/// ## Install on ArchLinux
86///
87/// ```bash
88/// pacman -S systemd
89/// ```
90pub struct JournaldSink {
91    prop: SinkProp,
92}
93
94impl JournaldSink {
95    const SYSLOG_LEVELS: SyslogLevels = SyslogLevels::new();
96
97    /// Gets a builder of `JournaldSink` with default parameters:
98    ///
99    /// | Parameter       | Default Value               |
100    /// |-----------------|-----------------------------|
101    /// | [level_filter]  | `All`                       |
102    /// | [formatter]     | `JournaldFormatter`         |
103    /// | [error_handler] | [`ErrorHandler::default()`] |
104    ///
105    /// [level_filter]: JournaldSinkBuilder::level_filter
106    /// [formatter]: JournaldSinkBuilder::formatter
107    /// [error_handler]: JournaldSinkBuilder::error_handler
108    /// [`ErrorHandler::default()`]: crate::error::ErrorHandler::default()
109    #[must_use]
110    pub fn builder() -> JournaldSinkBuilder {
111        let prop = SinkProp::default();
112        prop.set_formatter(JournaldFormatter::new());
113
114        JournaldSinkBuilder { prop }
115    }
116}
117
118impl GetSinkProp for JournaldSink {
119    fn prop(&self) -> &SinkProp {
120        &self.prop
121    }
122}
123
124impl Sink for JournaldSink {
125    fn log(&self, record: &Record) -> Result<()> {
126        let mut string_buf = StringBuf::new();
127        let mut ctx = FormatterContext::new();
128        self.prop
129            .formatter()
130            .format(record, &mut string_buf, &mut ctx)?;
131
132        let kvs = [
133            format!("MESSAGE={string_buf}"),
134            format!(
135                "PRIORITY={}",
136                JournaldSink::SYSLOG_LEVELS.level(record.level()) as u32
137            ),
138            format!("TID={}", record.tid()),
139        ];
140
141        let srcloc_kvs = match record.source_location() {
142            Some(srcloc) => [
143                Some(format!("CODE_FILE={}", srcloc.file_name())),
144                Some(format!("CODE_LINE={}", srcloc.line())),
145            ],
146            None => [None, None],
147        };
148
149        journal_send(kvs.iter().chain(srcloc_kvs.iter().flatten())).map_err(Error::WriteRecord)
150    }
151
152    fn flush(&self) -> Result<()> {
153        Ok(())
154    }
155}
156
157#[allow(missing_docs)]
158pub struct JournaldSinkBuilder {
159    prop: SinkProp,
160}
161
162impl JournaldSinkBuilder {
163    // Prop
164    //
165
166    /// Specifies a log level filter.
167    ///
168    /// This parameter is **optional**.
169    #[must_use]
170    pub fn level_filter(self, level_filter: LevelFilter) -> Self {
171        self.prop.set_level_filter(level_filter);
172        self
173    }
174
175    /// Specifies a formatter.
176    ///
177    /// This parameter is **optional**.
178    #[must_use]
179    pub fn formatter<F>(self, formatter: F) -> Self
180    where
181        F: Formatter + 'static,
182    {
183        self.prop.set_formatter(formatter);
184        self
185    }
186
187    /// Specifies an error handler.
188    ///
189    /// This parameter is **optional**.
190    #[must_use]
191    pub fn error_handler<F: Into<ErrorHandler>>(self, handler: F) -> Self {
192        self.prop.set_error_handler(handler);
193        self
194    }
195
196    //
197
198    /// Builds a [`JournaldSink`].
199    pub fn build(self) -> Result<JournaldSink> {
200        let sink = JournaldSink { prop: self.prop };
201        Ok(sink)
202    }
203}