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}