🛈 Note: This is pre-release documentation for the upcoming tracing 0.2.0 ecosystem.

For the release documentation, please see docs.rs, instead.

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