logforth_append_syslog/
lib.rs

1// Copyright 2024 FastLabs Developers
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Appender for writing log records to syslog.
16//!
17//! # Examples
18//!
19//!```rust, no_run
20//! use logforth_append_syslog::Syslog;
21//! use logforth_append_syslog::SyslogBuilder;
22//! use logforth_core::record::LevelFilter;
23//!
24//! let append = SyslogBuilder::tcp_well_known().unwrap().build();
25//!
26//! logforth_core::builder()
27//!     .dispatch(|d| d.filter(LevelFilter::Trace).append(append))
28//!     .apply();
29//!
30//! log::info!("This log will be written to syslog.");
31//! ```
32
33#![cfg_attr(docsrs, feature(doc_cfg))]
34
35use std::io;
36use std::sync::Mutex;
37use std::sync::MutexGuard;
38
39use fasyslog::SDElement;
40use fasyslog::format::SyslogContext;
41use fasyslog::sender::SyslogSender;
42use logforth_core::Append;
43use logforth_core::Diagnostic;
44use logforth_core::Error;
45use logforth_core::Layout;
46use logforth_core::record::Level;
47use logforth_core::record::Record;
48
49pub extern crate fasyslog;
50
51/// The format of the syslog message.
52#[derive(Debug, Copy, Clone)]
53pub enum SyslogFormat {
54    /// [RFC 3614] (BSD syslog Protocol)
55    ///
56    /// [RFC 3164]: https://datatracker.ietf.org/doc/html/rfc3164
57    RFC3164,
58    /// [RFC 5424] (The Syslog Protocol)
59    ///
60    /// [RFC 5424]: https://datatracker.ietf.org/doc/html/rfc5424
61    RFC5424,
62}
63
64/// A builder to configure and create an [`Syslog`] appender.
65#[derive(Debug)]
66pub struct SyslogBuilder {
67    sender: SyslogSender,
68    formatter: SyslogFormatter,
69}
70
71impl SyslogBuilder {
72    /// Create a new builder.
73    pub fn new(sender: SyslogSender) -> Self {
74        Self {
75            sender,
76            formatter: SyslogFormatter {
77                format: SyslogFormat::RFC3164,
78                context: SyslogContext::default(),
79                layout: None,
80            },
81        }
82    }
83
84    /// Build the [`Syslog`] appender.
85    pub fn build(self) -> Syslog {
86        let SyslogBuilder { sender, formatter } = self;
87        Syslog::new(sender, formatter)
88    }
89
90    /// Set the format of the [`Syslog`] appender.
91    pub fn format(mut self, format: SyslogFormat) -> Self {
92        self.formatter.format = format;
93        self
94    }
95
96    /// Set the context of the [`Syslog`] appender.
97    pub fn context(mut self, context: SyslogContext) -> Self {
98        self.formatter.context = context;
99        self
100    }
101
102    /// Set the layout of the [`Syslog`] appender.
103    ///
104    /// Default to `None`, the message will construct with only [`Record::payload`].
105    pub fn layout(mut self, layout: impl Into<Box<dyn Layout>>) -> Self {
106        self.formatter.layout = Some(layout.into());
107        self
108    }
109
110    /// Create a new syslog writer that sends messages to the well-known TCP port (514).
111    pub fn tcp_well_known() -> io::Result<SyslogBuilder> {
112        fasyslog::sender::tcp_well_known()
113            .map(SyslogSender::Tcp)
114            .map(Self::new)
115    }
116
117    /// Create a new syslog writer that sends messages to the given TCP address.
118    pub fn tcp<A: std::net::ToSocketAddrs>(addr: A) -> io::Result<SyslogBuilder> {
119        fasyslog::sender::tcp(addr)
120            .map(SyslogSender::Tcp)
121            .map(Self::new)
122    }
123
124    /// Create a new syslog writer that sends messages to the well-known UDP port (514).
125    pub fn udp_well_known() -> io::Result<SyslogBuilder> {
126        fasyslog::sender::udp_well_known()
127            .map(SyslogSender::Udp)
128            .map(Self::new)
129    }
130
131    /// Create a new syslog writer that sends messages to the given UDP address.
132    pub fn udp<L: std::net::ToSocketAddrs, R: std::net::ToSocketAddrs>(
133        local: L,
134        remote: R,
135    ) -> io::Result<SyslogBuilder> {
136        fasyslog::sender::udp(local, remote)
137            .map(SyslogSender::Udp)
138            .map(Self::new)
139    }
140
141    /// Create a new syslog writer that broadcast messages to the well-known UDP port (514).
142    pub fn broadcast_well_known() -> io::Result<SyslogBuilder> {
143        fasyslog::sender::broadcast_well_known()
144            .map(SyslogSender::Udp)
145            .map(Self::new)
146    }
147
148    /// Create a new syslog writer that broadcast messages to the given UDP address.
149    pub fn broadcast(port: u16) -> io::Result<SyslogBuilder> {
150        fasyslog::sender::broadcast(port)
151            .map(SyslogSender::Udp)
152            .map(Self::new)
153    }
154}
155
156#[cfg(feature = "rustls")]
157mod rustls_ext {
158    use std::io;
159    use std::net::ToSocketAddrs;
160    use std::sync::Arc;
161
162    use fasyslog::sender::SyslogSender;
163    use fasyslog::sender::rustls::ClientConfig;
164
165    use super::SyslogBuilder;
166
167    impl SyslogBuilder {
168        /// Create a TLS sender that sends messages to the well-known port (6514).
169        pub fn rustls_well_known<S: Into<String>>(domain: S) -> io::Result<SyslogBuilder> {
170            fasyslog::sender::rustls_well_known(domain)
171                .map(Box::new)
172                .map(SyslogSender::RustlsSender)
173                .map(Self::new)
174        }
175
176        /// Create a TLS sender that sends messages to the given address.
177        pub fn rustls<A: ToSocketAddrs, S: Into<String>>(
178            addr: A,
179            domain: S,
180        ) -> io::Result<SyslogBuilder> {
181            fasyslog::sender::rustls(addr, domain)
182                .map(Box::new)
183                .map(SyslogSender::RustlsSender)
184                .map(Self::new)
185        }
186
187        /// Create a TLS sender that sends messages to the given address with certificate builder.
188        pub fn rustls_with<A: ToSocketAddrs, S: Into<String>>(
189            addr: A,
190            domain: S,
191            config: Arc<ClientConfig>,
192        ) -> io::Result<SyslogBuilder> {
193            fasyslog::sender::rustls_with(addr, domain, config)
194                .map(Box::new)
195                .map(SyslogSender::RustlsSender)
196                .map(Self::new)
197        }
198    }
199}
200
201#[cfg(feature = "native-tls")]
202mod native_tls_ext {
203    use std::io;
204    use std::net::ToSocketAddrs;
205
206    use fasyslog::sender::SyslogSender;
207    use fasyslog::sender::native_tls::TlsConnectorBuilder;
208
209    use super::SyslogBuilder;
210
211    impl SyslogBuilder {
212        /// Create a TLS sender that sends messages to the well-known port (6514).
213        pub fn native_tls_well_known<S: AsRef<str>>(domain: S) -> io::Result<SyslogBuilder> {
214            fasyslog::sender::native_tls_well_known(domain)
215                .map(SyslogSender::NativeTlsSender)
216                .map(Self::new)
217        }
218
219        /// Create a TLS sender that sends messages to the given address.
220        pub fn native_tls<A: ToSocketAddrs, S: AsRef<str>>(
221            addr: A,
222            domain: S,
223        ) -> io::Result<SyslogBuilder> {
224            fasyslog::sender::native_tls(addr, domain)
225                .map(SyslogSender::NativeTlsSender)
226                .map(Self::new)
227        }
228
229        /// Create a TLS sender that sends messages to the given address with certificate builder.
230        pub fn native_tls_with<A: ToSocketAddrs, S: AsRef<str>>(
231            addr: A,
232            domain: S,
233            builder: TlsConnectorBuilder,
234        ) -> io::Result<SyslogBuilder> {
235            fasyslog::sender::native_tls_with(addr, domain, builder)
236                .map(SyslogSender::NativeTlsSender)
237                .map(Self::new)
238        }
239    }
240}
241
242#[cfg(unix)]
243mod unix_ext {
244    use std::io;
245
246    use fasyslog::sender::SyslogSender;
247
248    use super::SyslogBuilder;
249
250    impl SyslogBuilder {
251        /// Create a new syslog writer that sends messages to the given Unix stream socket.
252        pub fn unix_stream(path: impl AsRef<std::path::Path>) -> io::Result<SyslogBuilder> {
253            fasyslog::sender::unix_stream(path)
254                .map(SyslogSender::UnixStream)
255                .map(Self::new)
256        }
257
258        /// Create a new syslog writer that sends messages to the given Unix datagram socket.
259        pub fn unix_datagram(path: impl AsRef<std::path::Path>) -> io::Result<SyslogBuilder> {
260            fasyslog::sender::unix_datagram(path)
261                .map(SyslogSender::UnixDatagram)
262                .map(Self::new)
263        }
264
265        /// Create a new syslog writer that sends messages to the given Unix socket.
266        ///
267        /// This method will automatically choose between `unix_stream` and `unix_datagram` based on
268        /// the path.
269        pub fn unix(path: impl AsRef<std::path::Path>) -> io::Result<SyslogBuilder> {
270            fasyslog::sender::unix(path).map(Self::new)
271        }
272    }
273}
274
275/// An appender that writes log records to syslog.
276#[derive(Debug)]
277pub struct Syslog {
278    sender: Mutex<SyslogSender>,
279    formatter: SyslogFormatter,
280}
281
282impl Syslog {
283    /// Creates a new [`Syslog`] appender.
284    fn new(sender: SyslogSender, formatter: SyslogFormatter) -> Self {
285        let sender = Mutex::new(sender);
286        Self { sender, formatter }
287    }
288
289    fn sender(&self) -> MutexGuard<'_, SyslogSender> {
290        self.sender.lock().unwrap_or_else(|e| e.into_inner())
291    }
292}
293
294impl Append for Syslog {
295    fn append(&self, record: &Record, diags: &[Box<dyn Diagnostic>]) -> Result<(), Error> {
296        let message = self.formatter.format_message(record, diags)?;
297        let mut sender = self.sender();
298        sender
299            .send_formatted(&message)
300            .map_err(Error::from_io_error)?;
301        Ok(())
302    }
303
304    fn flush(&self) -> Result<(), Error> {
305        let mut sender = self.sender();
306        sender.flush().map_err(Error::from_io_error)?;
307        Ok(())
308    }
309}
310
311impl Drop for Syslog {
312    fn drop(&mut self) {
313        let sender = self.sender.get_mut().unwrap_or_else(|e| e.into_inner());
314        let _ = sender.flush();
315    }
316}
317
318#[derive(Debug)]
319struct SyslogFormatter {
320    format: SyslogFormat,
321    context: SyslogContext,
322    layout: Option<Box<dyn Layout>>,
323}
324
325fn log_level_to_syslog_severity(level: Level) -> fasyslog::Severity {
326    match level {
327        Level::Error => fasyslog::Severity::ERROR,
328        Level::Warn => fasyslog::Severity::WARNING,
329        Level::Info => fasyslog::Severity::NOTICE,
330        Level::Debug => fasyslog::Severity::INFORMATIONAL,
331        Level::Trace => fasyslog::Severity::DEBUG,
332    }
333}
334
335impl SyslogFormatter {
336    fn format_message(
337        &self,
338        record: &Record,
339        diags: &[Box<dyn Diagnostic>],
340    ) -> Result<Vec<u8>, Error> {
341        let severity = log_level_to_syslog_severity(record.level());
342
343        let message = match self.format {
344            SyslogFormat::RFC3164 => match self.layout {
345                None => format!(
346                    "{}",
347                    self.context
348                        .format_rfc3164(severity, Some(record.payload()))
349                ),
350                Some(ref layout) => {
351                    let message = layout.format(record, diags)?;
352                    let message = String::from_utf8_lossy(&message);
353                    format!("{}", self.context.format_rfc3164(severity, Some(message)))
354                }
355            },
356            SyslogFormat::RFC5424 => {
357                const EMPTY_MSGID: Option<&str> = None;
358                const EMPTY_STRUCTURED_DATA: Vec<SDElement> = Vec::new();
359
360                match self.layout {
361                    None => format!(
362                        "{}",
363                        self.context.format_rfc5424(
364                            severity,
365                            EMPTY_MSGID,
366                            EMPTY_STRUCTURED_DATA,
367                            Some(record.payload())
368                        )
369                    ),
370                    Some(ref layout) => {
371                        let message = layout.format(record, diags)?;
372                        let message = String::from_utf8_lossy(&message);
373                        format!(
374                            "{}",
375                            self.context.format_rfc5424(
376                                severity,
377                                EMPTY_MSGID,
378                                EMPTY_STRUCTURED_DATA,
379                                Some(message)
380                            )
381                        )
382                    }
383                }
384            }
385        };
386
387        Ok(message.into_bytes())
388    }
389}