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}