Skip to main content

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