spdlog/sink/
journald_sink.rs

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