tracing_journald/
lib.rs

1//! # tracing-journald
2//!
3//! Support for logging [`tracing`] events natively to [journald],
4//! preserving structured information.
5//!
6//! ## Overview
7//!
8//! [`tracing`] is a framework for instrumenting Rust programs to collect
9//! scoped, structured, and async-aware diagnostics. `tracing-journald` provides a
10//! [`tracing_subscriber::Layer`] implementation for logging `tracing` spans
11//! and events to [`systemd-journald`][journald], on Linux distributions that
12//! use `systemd`.
13//!
14//! *Compiler support: [requires `rustc` 1.65+][msrv]*
15//!
16//! [msrv]: #supported-rust-versions
17//! [`tracing`]: https://crates.io/crates/tracing
18//! [journald]: https://www.freedesktop.org/software/systemd/man/systemd-journald.service.html
19//!
20//! ## Supported Rust Versions
21//!
22//! Tracing is built against the latest stable release. The minimum supported
23//! version is 1.65. The current Tracing version is not guaranteed to build on
24//! Rust versions earlier than the minimum supported version.
25//!
26//! Tracing follows the same compiler support policies as the rest of the Tokio
27//! project. The current stable Rust compiler and the three most recent minor
28//! versions before it will always be supported. For example, if the current
29//! stable compiler version is 1.69, the minimum supported version will not be
30//! increased past 1.66, three minor versions prior. Increasing the minimum
31//! supported compiler version is not considered a semver breaking change as
32//! long as doing so complies with this policy.
33//!
34#![doc(
35    html_logo_url = "https://raw.githubusercontent.com/tokio-rs/tracing/main/assets/logo-type.png",
36    html_favicon_url = "https://raw.githubusercontent.com/tokio-rs/tracing/main/assets/favicon.ico",
37    issue_tracker_base_url = "https://github.com/tokio-rs/tracing/issues/"
38)]
39#![cfg_attr(docsrs, deny(rustdoc::broken_intra_doc_links))]
40#[cfg(unix)]
41use std::os::unix::net::UnixDatagram;
42use std::{fmt, io, io::Write};
43
44use tracing_core::{
45    event::Event,
46    field::Visit,
47    span::{Attributes, Id, Record},
48    Field, Level, Metadata, Subscriber,
49};
50use tracing_subscriber::{layer::Context, registry::LookupSpan};
51
52#[cfg(target_os = "linux")]
53mod memfd;
54#[cfg(target_os = "linux")]
55mod socket;
56
57/// Sends events and their fields to journald
58///
59/// [journald conventions] for structured field names differ from typical tracing idioms, and journald
60/// discards fields which violate its conventions. Hence, this layer automatically sanitizes field
61/// names by translating `.`s into `_`s, stripping leading `_`s and non-ascii-alphanumeric
62/// characters other than `_`, and upcasing.
63///
64/// By default, levels are mapped losslessly to journald `PRIORITY` values as follows:
65///
66/// - `ERROR` => Error (3)
67/// - `WARN` => Warning (4)
68/// - `INFO` => Notice (5)
69/// - `DEBUG` => Informational (6)
70/// - `TRACE` => Debug (7)
71///
72/// These mappings can be changed with [`Layer::with_priority_mappings`].
73///
74/// The standard journald `CODE_LINE` and `CODE_FILE` fields are automatically emitted. A `TARGET`
75/// field is emitted containing the event's target.
76///
77/// For events recorded inside spans, an additional `SPAN_NAME` field is emitted with the name of
78/// each of the event's parent spans.
79///
80/// User-defined fields other than the event `message` field have a prefix applied by default to
81/// prevent collision with standard fields.
82///
83/// [journald conventions]: https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html
84pub struct Layer {
85    #[cfg(unix)]
86    socket: UnixDatagram,
87    field_prefix: Option<String>,
88    syslog_identifier: String,
89    additional_fields: Vec<u8>,
90    priority_mappings: PriorityMappings,
91}
92
93#[cfg(unix)]
94const JOURNALD_PATH: &str = "/run/systemd/journal/socket";
95
96impl Layer {
97    /// Construct a journald layer
98    ///
99    /// Fails if the journald socket couldn't be opened. Returns a `NotFound` error unconditionally
100    /// in non-Unix environments.
101    pub fn new() -> io::Result<Self> {
102        #[cfg(unix)]
103        {
104            use std::path::Path;
105
106            let socket = UnixDatagram::unbound()?;
107            let layer = Self {
108                socket,
109                field_prefix: Some("F".into()),
110                syslog_identifier: std::env::args_os()
111                    .next()
112                    .as_ref()
113                    .and_then(|p| Path::new(p).file_name())
114                    .map(|n| n.to_string_lossy().into_owned())
115                    // If we fail to get the name of the current executable fall back to an empty string.
116                    .unwrap_or_default(),
117                additional_fields: Vec::new(),
118                priority_mappings: PriorityMappings::new(),
119            };
120            // Check that we can talk to journald, by sending empty payload which journald discards.
121            // However if the socket didn't exist or if none listened we'd get an error here.
122            layer.send_payload(&[])?;
123            Ok(layer)
124        }
125        #[cfg(not(unix))]
126        Err(io::Error::new(
127            io::ErrorKind::NotFound,
128            "journald does not exist in this environment",
129        ))
130    }
131
132    /// Sets the prefix to apply to names of user-defined fields other than the event `message`
133    /// field. Defaults to `Some("F")`.
134    pub fn with_field_prefix(mut self, x: Option<String>) -> Self {
135        self.field_prefix = x;
136        self
137    }
138
139    /// Sets how [`tracing_core::Level`]s are mapped to [journald priorities](Priority).
140    ///
141    /// # Examples
142    ///
143    /// ```rust
144    /// use tracing_journald::{Priority, PriorityMappings};
145    /// use tracing_subscriber::prelude::*;
146    /// use tracing::error;
147    ///
148    /// let registry = tracing_subscriber::registry();
149    /// match tracing_journald::layer() {
150    ///     Ok(layer) => {
151    ///         registry.with(
152    ///             layer
153    ///                 // We can tweak the mappings between the trace level and
154    ///                 // the journal priorities.
155    ///                 .with_priority_mappings(PriorityMappings {
156    ///                     info: Priority::Informational,
157    ///                     ..PriorityMappings::new()
158    ///                 }),
159    ///         );
160    ///     }
161    ///     // journald is typically available on Linux systems, but nowhere else. Portable software
162    ///     // should handle its absence gracefully.
163    ///     Err(e) => {
164    ///         registry.init();
165    ///         error!("couldn't connect to journald: {}", e);
166    ///     }
167    /// }
168    /// ```
169    pub fn with_priority_mappings(mut self, mappings: PriorityMappings) -> Self {
170        self.priority_mappings = mappings;
171        self
172    }
173
174    /// Sets the syslog identifier for this logger.
175    ///
176    /// The syslog identifier comes from the classic syslog interface (`openlog()`
177    /// and `syslog()`) and tags log entries with a given identifier.
178    /// Systemd exposes it in the `SYSLOG_IDENTIFIER` journal field, and allows
179    /// filtering log messages by syslog identifier with `journalctl -t`.
180    /// Unlike the unit (`journalctl -u`) this field is not trusted, i.e. applications
181    /// can set it freely, and use it e.g. to further categorize log entries emitted under
182    /// the same systemd unit or in the same process.  It also allows to filter for log
183    /// entries of processes not started in their own unit.
184    ///
185    /// See [Journal Fields](https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html)
186    /// and [journalctl](https://www.freedesktop.org/software/systemd/man/journalctl.html)
187    /// for more information.
188    ///
189    /// Defaults to the file name of the executable of the current process, if any.
190    pub fn with_syslog_identifier(mut self, identifier: String) -> Self {
191        self.syslog_identifier = identifier;
192        self
193    }
194
195    /// Adds fields that will get be passed to journald with every log entry.
196    ///
197    /// The input values of this function are interpreted as `(field, value)` pairs.
198    ///
199    /// This can for example be used to configure the syslog facility.
200    /// See [Journal Fields](https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html)
201    /// and [journalctl](https://www.freedesktop.org/software/systemd/man/journalctl.html)
202    /// for more information.
203    ///
204    /// Fields specified using this method will be added to the journald
205    /// message alongside fields generated from the event's fields, its
206    /// metadata, and the span context. If the name of a field provided using
207    /// this method is the same as the name of a field generated by the
208    /// layer, both fields will be sent to journald.
209    ///
210    /// ```no_run
211    /// # use tracing_journald::Layer;
212    /// let layer = Layer::new()
213    ///     .unwrap()
214    ///     .with_custom_fields([("SYSLOG_FACILITY", "17")]);
215    /// ```
216    ///
217    pub fn with_custom_fields<T: AsRef<str>, U: AsRef<[u8]>>(
218        mut self,
219        fields: impl IntoIterator<Item = (T, U)>,
220    ) -> Self {
221        for (name, value) in fields {
222            put_field_length_encoded(&mut self.additional_fields, name.as_ref(), |buf| {
223                buf.extend_from_slice(value.as_ref())
224            })
225        }
226        self
227    }
228
229    /// Returns the syslog identifier in use.
230    pub fn syslog_identifier(&self) -> &str {
231        &self.syslog_identifier
232    }
233
234    #[cfg(not(unix))]
235    fn send_payload(&self, _opayload: &[u8]) -> io::Result<()> {
236        Err(io::Error::new(
237            io::ErrorKind::Other,
238            "journald not supported on non-Unix",
239        ))
240    }
241
242    #[cfg(unix)]
243    fn send_payload(&self, payload: &[u8]) -> io::Result<usize> {
244        self.socket
245            .send_to(payload, JOURNALD_PATH)
246            .or_else(|error| {
247                if Some(libc::EMSGSIZE) == error.raw_os_error() {
248                    self.send_large_payload(payload)
249                } else {
250                    Err(error)
251                }
252            })
253    }
254
255    #[cfg(all(unix, not(target_os = "linux")))]
256    fn send_large_payload(&self, _payload: &[u8]) -> io::Result<usize> {
257        Err(io::Error::new(
258            io::ErrorKind::Other,
259            "Large payloads not supported on non-Linux OS",
260        ))
261    }
262
263    /// Send large payloads to journald via a memfd.
264    #[cfg(target_os = "linux")]
265    fn send_large_payload(&self, payload: &[u8]) -> io::Result<usize> {
266        // If the payload's too large for a single datagram, send it through a memfd, see
267        // https://systemd.io/JOURNAL_NATIVE_PROTOCOL/
268        use std::os::unix::prelude::AsRawFd;
269        // Write the whole payload to a memfd
270        let mut mem = memfd::create_sealable()?;
271        mem.write_all(payload)?;
272        // Fully seal the memfd to signal journald that its backing data won't resize anymore
273        // and so is safe to mmap.
274        memfd::seal_fully(mem.as_raw_fd())?;
275        socket::send_one_fd_to(&self.socket, mem.as_raw_fd(), JOURNALD_PATH)
276    }
277
278    fn put_priority(&self, buf: &mut Vec<u8>, meta: &Metadata<'_>) {
279        put_field_wellformed(
280            buf,
281            "PRIORITY",
282            &[match *meta.level() {
283                Level::ERROR => self.priority_mappings.error as u8,
284                Level::WARN => self.priority_mappings.warn as u8,
285                Level::INFO => self.priority_mappings.info as u8,
286                Level::DEBUG => self.priority_mappings.debug as u8,
287                Level::TRACE => self.priority_mappings.trace as u8,
288            }],
289        );
290    }
291}
292
293/// Construct a journald layer
294///
295/// Fails if the journald socket couldn't be opened.
296pub fn layer() -> io::Result<Layer> {
297    Layer::new()
298}
299
300impl<S> tracing_subscriber::Layer<S> for Layer
301where
302    S: Subscriber + for<'span> LookupSpan<'span>,
303{
304    fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
305        let span = ctx.span(id).expect("unknown span");
306        let mut buf = Vec::with_capacity(256);
307
308        writeln!(buf, "SPAN_NAME").unwrap();
309        put_value(&mut buf, span.name().as_bytes());
310        put_metadata(&mut buf, span.metadata(), Some("SPAN_"));
311
312        attrs.record(&mut SpanVisitor {
313            buf: &mut buf,
314            field_prefix: self.field_prefix.as_deref(),
315        });
316
317        span.extensions_mut().insert(SpanFields(buf));
318    }
319
320    fn on_record(&self, id: &Id, values: &Record<'_>, ctx: Context<'_, S>) {
321        let span = ctx.span(id).expect("unknown span");
322        let mut exts = span.extensions_mut();
323        let buf = &mut exts.get_mut::<SpanFields>().expect("missing fields").0;
324        values.record(&mut SpanVisitor {
325            buf,
326            field_prefix: self.field_prefix.as_deref(),
327        });
328    }
329
330    fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
331        let mut buf = Vec::with_capacity(256);
332
333        // Record span fields
334        for span in ctx
335            .lookup_current()
336            .into_iter()
337            .flat_map(|span| span.scope().from_root())
338        {
339            let exts = span.extensions();
340            let fields = exts.get::<SpanFields>().expect("missing fields");
341            buf.extend_from_slice(&fields.0);
342        }
343
344        // Record event fields
345        self.put_priority(&mut buf, event.metadata());
346        put_metadata(&mut buf, event.metadata(), None);
347        put_field_length_encoded(&mut buf, "SYSLOG_IDENTIFIER", |buf| {
348            write!(buf, "{}", self.syslog_identifier).unwrap()
349        });
350        buf.extend_from_slice(&self.additional_fields);
351
352        event.record(&mut EventVisitor::new(
353            &mut buf,
354            self.field_prefix.as_deref(),
355        ));
356
357        // At this point we can't handle the error anymore so just ignore it.
358        let _ = self.send_payload(&buf);
359    }
360}
361
362struct SpanFields(Vec<u8>);
363
364struct SpanVisitor<'a> {
365    buf: &'a mut Vec<u8>,
366    field_prefix: Option<&'a str>,
367}
368
369impl SpanVisitor<'_> {
370    fn put_span_prefix(&mut self) {
371        if let Some(prefix) = self.field_prefix {
372            self.buf.extend_from_slice(prefix.as_bytes());
373            self.buf.push(b'_');
374        }
375    }
376}
377
378impl Visit for SpanVisitor<'_> {
379    fn record_str(&mut self, field: &Field, value: &str) {
380        self.put_span_prefix();
381        put_field_length_encoded(self.buf, field.name(), |buf| {
382            buf.extend_from_slice(value.as_bytes())
383        });
384    }
385
386    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
387        self.put_span_prefix();
388        put_field_length_encoded(self.buf, field.name(), |buf| {
389            write!(buf, "{:?}", value).unwrap()
390        });
391    }
392}
393
394/// Helper for generating the journal export format, which is consumed by journald:
395/// https://www.freedesktop.org/wiki/Software/systemd/export/
396struct EventVisitor<'a> {
397    buf: &'a mut Vec<u8>,
398    prefix: Option<&'a str>,
399}
400
401impl<'a> EventVisitor<'a> {
402    fn new(buf: &'a mut Vec<u8>, prefix: Option<&'a str>) -> Self {
403        Self { buf, prefix }
404    }
405
406    fn put_prefix(&mut self, field: &Field) {
407        if let Some(prefix) = self.prefix {
408            if field.name() != "message" {
409                // message maps to the standard MESSAGE field so don't prefix it
410                self.buf.extend_from_slice(prefix.as_bytes());
411                self.buf.push(b'_');
412            }
413        }
414    }
415}
416
417impl Visit for EventVisitor<'_> {
418    fn record_str(&mut self, field: &Field, value: &str) {
419        self.put_prefix(field);
420        put_field_length_encoded(self.buf, field.name(), |buf| {
421            buf.extend_from_slice(value.as_bytes())
422        });
423    }
424
425    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
426        self.put_prefix(field);
427        put_field_length_encoded(self.buf, field.name(), |buf| {
428            write!(buf, "{:?}", value).unwrap()
429        });
430    }
431}
432
433/// A priority (called "severity code" by syslog) is used to mark the
434/// importance of a message.
435///
436/// Descriptions and examples are taken from the [Arch Linux wiki].
437/// Priorities are also documented in the
438/// [section 6.2.1 of the Syslog protocol RFC][syslog].
439///
440/// [Arch Linux wiki]: https://wiki.archlinux.org/title/Systemd/Journal#Priority_level
441/// [syslog]: https://www.rfc-editor.org/rfc/rfc5424#section-6.2.1
442#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
443#[repr(u8)]
444pub enum Priority {
445    /// System is unusable.
446    ///
447    /// Examples:
448    ///
449    /// - severe Kernel BUG
450    /// - systemd dumped core
451    ///
452    /// This level should not be used by applications.
453    Emergency = b'0',
454    /// Should be corrected immediately.
455    ///
456    /// Examples:
457    ///
458    /// - Vital subsystem goes out of work, data loss:
459    /// - `kernel: BUG: unable to handle kernel paging request at ffffc90403238ffc`
460    Alert = b'1',
461    /// Critical conditions
462    ///
463    /// Examples:
464    ///
465    /// - Crashe, coredumps
466    /// - `systemd-coredump[25319]: Process 25310 (plugin-container) of user 1000 dumped core`
467    Critical = b'2',
468    /// Error conditions
469    ///
470    /// Examples:
471    ///
472    /// - Not severe error reported
473    /// - `kernel: usb 1-3: 3:1: cannot get freq at ep 0x84, systemd[1]: Failed unmounting /var`
474    /// - `libvirtd[1720]: internal error: Failed to initialize a valid firewall backend`
475    Error = b'3',
476    /// May indicate that an error will occur if action is not taken.
477    ///
478    /// Examples:
479    ///
480    /// - a non-root file system has only 1GB free
481    /// - `org.freedesktop. Notifications[1860]: (process:5999): Gtk-WARNING **: Locale not supported by C library. Using the fallback 'C' locale`
482    Warning = b'4',
483    /// Events that are unusual, but not error conditions.
484    ///
485    /// Examples:
486    ///
487    /// - `systemd[1]: var.mount: Directory /var to mount over is not empty, mounting anyway`
488    /// - `gcr-prompter[4997]: Gtk: GtkDialog mapped without a transient parent. This is discouraged`
489    Notice = b'5',
490    /// Normal operational messages that require no action.
491    ///
492    /// Example: `lvm[585]: 7 logical volume(s) in volume group "archvg" now active`
493    Informational = b'6',
494    /// Information useful to developers for debugging the
495    /// application.
496    ///
497    /// Example: `kdeinit5[1900]: powerdevil: Scheduling inhibition from ":1.14" "firefox" with cookie 13 and reason "screen"`
498    Debug = b'7',
499}
500
501/// Mappings from tracing [`Level`]s to journald [priorities].
502///
503/// [priorities]: Priority
504#[derive(Debug, Clone)]
505pub struct PriorityMappings {
506    /// Priority mapped to the `ERROR` level
507    pub error: Priority,
508    /// Priority mapped to the `WARN` level
509    pub warn: Priority,
510    /// Priority mapped to the `INFO` level
511    pub info: Priority,
512    /// Priority mapped to the `DEBUG` level
513    pub debug: Priority,
514    /// Priority mapped to the `TRACE` level
515    pub trace: Priority,
516}
517
518impl PriorityMappings {
519    /// Returns the default priority mappings:
520    ///
521    /// - [`tracing::Level::ERROR`][]: [`Priority::Error`] (3)
522    /// - [`tracing::Level::WARN`][]: [`Priority::Warning`] (4)
523    /// - [`tracing::Level::INFO`][]: [`Priority::Notice`] (5)
524    /// - [`tracing::Level::DEBUG`][]: [`Priority::Informational`] (6)
525    /// - [`tracing::Level::TRACE`][]: [`Priority::Debug`] (7)
526    ///
527    /// [`tracing::Level::ERROR`]: tracing_core::Level::ERROR
528    /// [`tracing::Level::WARN`]: tracing_core::Level::WARN
529    /// [`tracing::Level::INFO`]: tracing_core::Level::INFO
530    /// [`tracing::Level::DEBUG`]: tracing_core::Level::DEBUG
531    /// [`tracing::Level::TRACE`]: tracing_core::Level::TRACE
532    pub fn new() -> PriorityMappings {
533        Self {
534            error: Priority::Error,
535            warn: Priority::Warning,
536            info: Priority::Notice,
537            debug: Priority::Informational,
538            trace: Priority::Debug,
539        }
540    }
541}
542
543impl Default for PriorityMappings {
544    fn default() -> Self {
545        Self::new()
546    }
547}
548
549fn put_metadata(buf: &mut Vec<u8>, meta: &Metadata<'_>, prefix: Option<&str>) {
550    if let Some(prefix) = prefix {
551        write!(buf, "{}", prefix).unwrap();
552    }
553    put_field_wellformed(buf, "TARGET", meta.target().as_bytes());
554    if let Some(file) = meta.file() {
555        if let Some(prefix) = prefix {
556            write!(buf, "{}", prefix).unwrap();
557        }
558        put_field_wellformed(buf, "CODE_FILE", file.as_bytes());
559    }
560    if let Some(line) = meta.line() {
561        if let Some(prefix) = prefix {
562            write!(buf, "{}", prefix).unwrap();
563        }
564        // Text format is safe as a line number can't possibly contain anything funny
565        writeln!(buf, "CODE_LINE={}", line).unwrap();
566    }
567}
568
569/// Append a sanitized and length-encoded field into `buf`.
570///
571/// Unlike `put_field_wellformed` this function handles arbitrary field names and values.
572///
573/// `name` denotes the field name. It gets sanitized before being appended to `buf`.
574///
575/// `write_value` is invoked with `buf` as argument to append the value data to `buf`.  It must
576/// not delete from `buf`, but may append arbitrary data.  This function then determines the length
577/// of the data written and adds it in the appropriate place in `buf`.
578fn put_field_length_encoded(buf: &mut Vec<u8>, name: &str, write_value: impl FnOnce(&mut Vec<u8>)) {
579    sanitize_name(name, buf);
580    buf.push(b'\n');
581    buf.extend_from_slice(&[0; 8]); // Length tag, to be populated
582    let start = buf.len();
583    write_value(buf);
584    let end = buf.len();
585    buf[start - 8..start].copy_from_slice(&((end - start) as u64).to_le_bytes());
586    buf.push(b'\n');
587}
588
589/// Mangle a name into journald-compliant form
590fn sanitize_name(name: &str, buf: &mut Vec<u8>) {
591    buf.extend(
592        name.bytes()
593            .map(|c| if c == b'.' { b'_' } else { c })
594            .skip_while(|&c| c == b'_')
595            .filter(|&c| c == b'_' || char::from(c).is_ascii_alphanumeric())
596            .map(|c| char::from(c).to_ascii_uppercase() as u8),
597    );
598}
599
600/// Append arbitrary data with a well-formed name and value.
601///
602/// `value` must not contain an internal newline, because this function writes
603/// `value` in the new-line separated format.
604///
605/// For a "newline-safe" variant, see `put_field_length_encoded`.
606fn put_field_wellformed(buf: &mut Vec<u8>, name: &str, value: &[u8]) {
607    buf.extend_from_slice(name.as_bytes());
608    buf.push(b'\n');
609    put_value(buf, value);
610}
611
612/// Write the value portion of a key-value pair, in newline separated format.
613///
614/// `value` must not contain an internal newline.
615///
616/// For a "newline-safe" variant, see `put_field_length_encoded`.
617fn put_value(buf: &mut Vec<u8>, value: &[u8]) {
618    buf.extend_from_slice(&(value.len() as u64).to_le_bytes());
619    buf.extend_from_slice(value);
620    buf.push(b'\n');
621}