tracing-journald-0.3.0/.cargo_vcs_info.json0000644000000001560000000000100143010ustar { "git": { "sha1": "63ce8588a4c806d56ce75dd955e65e9766fb76bb" }, "path_in_vcs": "tracing-journald" }tracing-journald-0.3.0/CHANGELOG.md000064400000000000000000000046700072674642500147370ustar 00000000000000# 0.3.0 (April 21, 2022) This is a breaking release which changes the format in which span fields are output to `journald`. Previously, span field names were prefixed with the depth of the span in the current trace tree. However, these prefixes are unnecessary, as `journald` has built in support for duplicated field names. See PR [#1986] for details on this change. ## Changed - Removed span field prefixes ([#1986]) - Renamed `S{num}_NAME` fields to `SPAN_NAME` ([#1986]) ### Fixed - Fixed broken links in documentation ([#2077]) Thanks to @wiktorsikora and @ben0x539 for contributing to this release! [#1986]: https://github.com/tokio-rs/tracing/pull/1986 [#2077]: https://github.com/tokio-rs/tracing/pull/2077 # 0.2.4 (March 17, 2022) ### Fixed - Fixed compilation error in `memfd_create_syscall` on 32-bit targets ([#1982]) Thanks to new contributor @chrta for contributing to this release! [#1982]: https://github.com/tokio-rs/tracing/pull/1982 # 0.2.3 (February 7, 2022) ### Fixed - Fixed missing `memfd_create` with `glibc` versions < 2.25 ([#1912]) ### Changed - Updated minimum supported Rust version to 1.49.0 ([#1913]) Thanks to @9999years for contributing to this release! [#1912]: https://github.com/tokio-rs/tracing/pull/1912 [#1913]: https://github.com/tokio-rs/tracing/pull/1913 # 0.2.2 (January 14, 2022) ### Added - Include a syslog identifier in log messages ([#1822]) - Added `Layer::with_syslog_identifier` method to override the syslog identifier ([#1822]) Thanks to @lunaryorn for contributing to this release! [#1822]: https://github.com/tokio-rs/tracing/pull/1822 # 0.2.1 (December 29, 2021) This release improves how `tracing-journald` communicates with `journald`, including the handling of large payloads. ### Added - Use an unconnected socket, so that logging can resume after a `journald` restart ([#1758]) ### Fixed - Fixed string values being written using `fmt::Debug` ([#1714]) - Fixed `EMSGSIZE` when log entries exceed a certain size ([#1744]) A huge thank-you to new contributor @lunaryorn, for contributing all of the changes in this release! [#1714]: https://github.com/tokio-rs/tracing/pull/1714 [#1744]: https://github.com/tokio-rs/tracing/pull/1744 [#1758]: https://github.com/tokio-rs/tracing/pull/1758 # 0.2.0 (October 22nd, 2021) ### Changed - Updated `tracing-subscriber` dependency to 0.3.0 ([#1677]) [#1677]: https://github.com/tokio-rs/tracing/pull/1677 # 0.1.0 (June 29, 2020) - Initial releasetracing-journald-0.3.0/Cargo.toml0000644000000023170000000000100123000ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" rust-version = "1.49.0" name = "tracing-journald" version = "0.3.0" authors = ["Benjamin Saunders "] description = "rich journald subscriber for `tracing`" homepage = "https://tokio.rs" readme = "README.md" keywords = [ "tracing", "journald", ] categories = [ "development-tools::debugging", "development-tools::profiling", ] license = "MIT" repository = "https://github.com/tokio-rs/tracing" [dependencies.libc] version = "0.2.107" [dependencies.tracing-core] version = "0.1.10" [dependencies.tracing-subscriber] version = "0.3" [dev-dependencies.serde] version = "1.0.130" features = ["derive"] [dev-dependencies.serde_json] version = "1.0.68" [dev-dependencies.tracing] version = "0.1" tracing-journald-0.3.0/Cargo.toml.orig000064400000000000000000000014000072674642500160010ustar 00000000000000[package] name = "tracing-journald" version = "0.3.0" authors = ["Benjamin Saunders "] edition = "2018" license = "MIT" readme = "README.md" repository = "https://github.com/tokio-rs/tracing" homepage = "https://tokio.rs" description = "rich journald subscriber for `tracing`" categories = [ "development-tools::debugging", "development-tools::profiling", ] keywords = ["tracing", "journald"] rust-version = "1.49.0" [dependencies] libc = "0.2.107" tracing-core = { path = "../tracing-core", version = "0.1.10" } tracing-subscriber = { path = "../tracing-subscriber", version = "0.3" } [dev-dependencies] serde_json = "1.0.68" serde = { version = "1.0.130", features = ["derive"] } tracing = { path = "../tracing", version = "0.1" }tracing-journald-0.3.0/LICENSE000064400000000000000000000020460072674642500141260ustar 00000000000000Copyright (c) 2019 Tokio Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tracing-journald-0.3.0/README.md000064400000000000000000000047240072674642500144050ustar 00000000000000![Tracing — Structured, application-level diagnostics][splash] [splash]: https://raw.githubusercontent.com/tokio-rs/tracing/master/assets/splash.svg # tracing-journald Support for logging [`tracing`] events natively to [journald], preserving structured information. [![Crates.io][crates-badge]][crates-url] [![Documentation (master)][docs-master-badge]][docs-master-url] [![MIT licensed][mit-badge]][mit-url] ![maintenance status][maint-badge] [crates-badge]: https://img.shields.io/crates/v/tracing-journald.svg [crates-url]: https://crates.io/crates/tracing-journald [docs-master-badge]: https://img.shields.io/badge/docs-master-blue [docs-master-url]: https://tracing-rs.netlify.com/tracing_journald [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg [mit-url]: LICENSE [maint-badge]: https://img.shields.io/badge/maintenance-experimental-blue.svg ## Overview [`tracing`] is a framework for instrumenting Rust programs to collect scoped, structured, and async-aware diagnostics. `tracing-journald` provides a [`tracing-subscriber::Layer`][layer] implementation for logging `tracing` spans and events to [`systemd-journald`][journald], on Linux distributions that use `systemd`. *Compiler support: [requires `rustc` 1.49+][msrv]* [msrv]: #supported-rust-versions [`tracing`]: https://crates.io/crates/tracing [layer]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/trait.Layer.html [journald]: https://www.freedesktop.org/software/systemd/man/systemd-journald.service.html ## Supported Rust Versions Tracing is built against the latest stable release. The minimum supported version is 1.49. The current Tracing version is not guaranteed to build on Rust versions earlier than the minimum supported version. Tracing follows the same compiler support policies as the rest of the Tokio project. The current stable Rust compiler and the three most recent minor versions before it will always be supported. For example, if the current stable compiler version is 1.45, the minimum supported version will not be increased past 1.42, three minor versions prior. Increasing the minimum supported compiler version is not considered a semver breaking change as long as doing so complies with this policy. ## License This project is licensed under the [MIT license](LICENSE). ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Tracing by you, shall be licensed as MIT, without any additional terms or conditions. tracing-journald-0.3.0/src/lib.rs000064400000000000000000000356250072674642500150350ustar 00000000000000//! # tracing-journald //! //! Support for logging [`tracing`] events natively to [journald], //! preserving structured information. //! //! ## Overview //! //! [`tracing`] is a framework for instrumenting Rust programs to collect //! scoped, structured, and async-aware diagnostics. `tracing-journald` provides a //! [`tracing-subscriber::Layer`] implementation for logging `tracing` spans //! and events to [`systemd-journald`][journald], on Linux distributions that //! use `systemd`. //! //! *Compiler support: [requires `rustc` 1.49+][msrv]* //! //! [msrv]: #supported-rust-versions //! [`tracing`]: https://crates.io/crates/tracing //! [journald]: https://www.freedesktop.org/software/systemd/man/systemd-journald.service.html //! //! ## Supported Rust Versions //! //! Tracing is built against the latest stable release. The minimum supported //! version is 1.49. The current Tracing version is not guaranteed to build on //! Rust versions earlier than the minimum supported version. //! //! Tracing follows the same compiler support policies as the rest of the Tokio //! project. The current stable Rust compiler and the three most recent minor //! versions before it will always be supported. For example, if the current //! stable compiler version is 1.45, the minimum supported version will not be //! increased past 1.42, three minor versions prior. Increasing the minimum //! supported compiler version is not considered a semver breaking change as //! long as doing so complies with this policy. //! #![doc( html_logo_url = "https://raw.githubusercontent.com/tokio-rs/tracing/master/assets/logo-type.png", issue_tracker_base_url = "https://github.com/tokio-rs/tracing/issues/" )] #![cfg_attr(docsrs, deny(rustdoc::broken_intra_doc_links))] #[cfg(unix)] use std::os::unix::net::UnixDatagram; use std::{fmt, io, io::Write}; use tracing_core::{ event::Event, field::Visit, span::{Attributes, Id, Record}, Field, Level, Metadata, Subscriber, }; use tracing_subscriber::{layer::Context, registry::LookupSpan}; #[cfg(target_os = "linux")] mod memfd; #[cfg(target_os = "linux")] mod socket; /// Sends events and their fields to journald /// /// [journald conventions] for structured field names differ from typical tracing idioms, and journald /// discards fields which violate its conventions. Hence, this layer automatically sanitizes field /// names by translating `.`s into `_`s, stripping leading `_`s and non-ascii-alphanumeric /// characters other than `_`, and upcasing. /// /// Levels are mapped losslessly to journald `PRIORITY` values as follows: /// /// - `ERROR` => Error (3) /// - `WARN` => Warning (4) /// - `INFO` => Notice (5) /// - `DEBUG` => Informational (6) /// - `TRACE` => Debug (7) /// /// The standard journald `CODE_LINE` and `CODE_FILE` fields are automatically emitted. A `TARGET` /// field is emitted containing the event's target. /// /// For events recorded inside spans, an additional `SPAN_NAME` field is emitted with the name of /// each of the event's parent spans. /// /// User-defined fields other than the event `message` field have a prefix applied by default to /// prevent collision with standard fields. /// /// [journald conventions]: https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html pub struct Layer { #[cfg(unix)] socket: UnixDatagram, field_prefix: Option, syslog_identifier: String, } #[cfg(unix)] const JOURNALD_PATH: &str = "/run/systemd/journal/socket"; impl Layer { /// Construct a journald layer /// /// Fails if the journald socket couldn't be opened. Returns a `NotFound` error unconditionally /// in non-Unix environments. pub fn new() -> io::Result { #[cfg(unix)] { let socket = UnixDatagram::unbound()?; let layer = Self { socket, field_prefix: Some("F".into()), syslog_identifier: std::env::current_exe() .ok() .as_ref() .and_then(|p| p.file_name()) .map(|n| n.to_string_lossy().into_owned()) // If we fail to get the name of the current executable fall back to an empty string. .unwrap_or_else(String::new), }; // Check that we can talk to journald, by sending empty payload which journald discards. // However if the socket didn't exist or if none listened we'd get an error here. layer.send_payload(&[])?; Ok(layer) } #[cfg(not(unix))] Err(io::Error::new( io::ErrorKind::NotFound, "journald does not exist in this environment", )) } /// Sets the prefix to apply to names of user-defined fields other than the event `message` /// field. Defaults to `Some("F")`. pub fn with_field_prefix(mut self, x: Option) -> Self { self.field_prefix = x; self } /// Sets the syslog identifier for this logger. /// /// The syslog identifier comes from the classic syslog interface (`openlog()` /// and `syslog()`) and tags log entries with a given identifier. /// Systemd exposes it in the `SYSLOG_IDENTIFIER` journal field, and allows /// filtering log messages by syslog identifier with `journalctl -t`. /// Unlike the unit (`journalctl -u`) this field is not trusted, i.e. applications /// can set it freely, and use it e.g. to further categorize log entries emitted under /// the same systemd unit or in the same process. It also allows to filter for log /// entries of processes not started in their own unit. /// /// See [Journal Fields](https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html) /// and [journalctl](https://www.freedesktop.org/software/systemd/man/journalctl.html) /// for more information. /// /// Defaults to the file name of the executable of the current process, if any. pub fn with_syslog_identifier(mut self, identifier: String) -> Self { self.syslog_identifier = identifier; self } /// Returns the syslog identifier in use. pub fn syslog_identifier(&self) -> &str { &self.syslog_identifier } #[cfg(not(unix))] fn send_payload(&self, _opayload: &[u8]) -> io::Result<()> { Err(io::Error::new( io::ErrorKind::Unsupported, "journald not supported on non-Unix", )) } #[cfg(unix)] fn send_payload(&self, payload: &[u8]) -> io::Result { self.socket .send_to(payload, JOURNALD_PATH) .or_else(|error| { if Some(libc::EMSGSIZE) == error.raw_os_error() { self.send_large_payload(payload) } else { Err(error) } }) } #[cfg(all(unix, not(target_os = "linux")))] fn send_large_payload(&self, _payload: &[u8]) -> io::Result { Err(io::Error::new( io::ErrorKind::Unsupported, "Large payloads not supported on non-Linux OS", )) } /// Send large payloads to journald via a memfd. #[cfg(target_os = "linux")] fn send_large_payload(&self, payload: &[u8]) -> io::Result { // If the payload's too large for a single datagram, send it through a memfd, see // https://systemd.io/JOURNAL_NATIVE_PROTOCOL/ use std::os::unix::prelude::AsRawFd; // Write the whole payload to a memfd let mut mem = memfd::create_sealable()?; mem.write_all(payload)?; // Fully seal the memfd to signal journald that its backing data won't resize anymore // and so is safe to mmap. memfd::seal_fully(mem.as_raw_fd())?; socket::send_one_fd_to(&self.socket, mem.as_raw_fd(), JOURNALD_PATH) } } /// Construct a journald layer /// /// Fails if the journald socket couldn't be opened. pub fn layer() -> io::Result { Layer::new() } impl tracing_subscriber::Layer for Layer where S: Subscriber + for<'span> LookupSpan<'span>, { fn on_new_span(&self, attrs: &Attributes, id: &Id, ctx: Context<'_, S>) { let span = ctx.span(id).expect("unknown span"); let mut buf = Vec::with_capacity(256); writeln!(buf, "SPAN_NAME").unwrap(); put_value(&mut buf, span.name().as_bytes()); put_metadata(&mut buf, span.metadata(), Some("SPAN_")); attrs.record(&mut SpanVisitor { buf: &mut buf, field_prefix: self.field_prefix.as_deref(), }); span.extensions_mut().insert(SpanFields(buf)); } fn on_record(&self, id: &Id, values: &Record, ctx: Context) { let span = ctx.span(id).expect("unknown span"); let mut exts = span.extensions_mut(); let buf = &mut exts.get_mut::().expect("missing fields").0; values.record(&mut SpanVisitor { buf, field_prefix: self.field_prefix.as_deref(), }); } fn on_event(&self, event: &Event, ctx: Context) { let mut buf = Vec::with_capacity(256); // Record span fields for span in ctx .lookup_current() .into_iter() .flat_map(|span| span.scope().from_root()) { let exts = span.extensions(); let fields = exts.get::().expect("missing fields"); buf.extend_from_slice(&fields.0); } // Record event fields put_priority(&mut buf, event.metadata()); put_metadata(&mut buf, event.metadata(), None); put_field_length_encoded(&mut buf, "SYSLOG_IDENTIFIER", |buf| { write!(buf, "{}", self.syslog_identifier).unwrap() }); event.record(&mut EventVisitor::new( &mut buf, self.field_prefix.as_deref(), )); // At this point we can't handle the error anymore so just ignore it. let _ = self.send_payload(&buf); } } struct SpanFields(Vec); struct SpanVisitor<'a> { buf: &'a mut Vec, field_prefix: Option<&'a str>, } impl SpanVisitor<'_> { fn put_span_prefix(&mut self) { if let Some(prefix) = self.field_prefix { self.buf.extend_from_slice(prefix.as_bytes()); self.buf.push(b'_'); } } } impl Visit for SpanVisitor<'_> { fn record_str(&mut self, field: &Field, value: &str) { self.put_span_prefix(); put_field_length_encoded(self.buf, field.name(), |buf| { buf.extend_from_slice(value.as_bytes()) }); } fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { self.put_span_prefix(); put_field_length_encoded(self.buf, field.name(), |buf| { write!(buf, "{:?}", value).unwrap() }); } } /// Helper for generating the journal export format, which is consumed by journald: /// https://www.freedesktop.org/wiki/Software/systemd/export/ struct EventVisitor<'a> { buf: &'a mut Vec, prefix: Option<&'a str>, } impl<'a> EventVisitor<'a> { fn new(buf: &'a mut Vec, prefix: Option<&'a str>) -> Self { Self { buf, prefix } } fn put_prefix(&mut self, field: &Field) { if let Some(prefix) = self.prefix { if field.name() != "message" { // message maps to the standard MESSAGE field so don't prefix it self.buf.extend_from_slice(prefix.as_bytes()); self.buf.push(b'_'); } } } } impl Visit for EventVisitor<'_> { fn record_str(&mut self, field: &Field, value: &str) { self.put_prefix(field); put_field_length_encoded(self.buf, field.name(), |buf| { buf.extend_from_slice(value.as_bytes()) }); } fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { self.put_prefix(field); put_field_length_encoded(self.buf, field.name(), |buf| { write!(buf, "{:?}", value).unwrap() }); } } fn put_priority(buf: &mut Vec, meta: &Metadata) { put_field_wellformed( buf, "PRIORITY", match *meta.level() { Level::ERROR => b"3", Level::WARN => b"4", Level::INFO => b"5", Level::DEBUG => b"6", Level::TRACE => b"7", }, ); } fn put_metadata(buf: &mut Vec, meta: &Metadata, prefix: Option<&str>) { if let Some(prefix) = prefix { write!(buf, "{}", prefix).unwrap(); } put_field_wellformed(buf, "TARGET", meta.target().as_bytes()); if let Some(file) = meta.file() { if let Some(prefix) = prefix { write!(buf, "{}", prefix).unwrap(); } put_field_wellformed(buf, "CODE_FILE", file.as_bytes()); } if let Some(line) = meta.line() { if let Some(prefix) = prefix { write!(buf, "{}", prefix).unwrap(); } // Text format is safe as a line number can't possibly contain anything funny writeln!(buf, "CODE_LINE={}", line).unwrap(); } } /// Append a sanitized and length-encoded field into `buf`. /// /// Unlike `put_field_wellformed` this function handles arbitrary field names and values. /// /// `name` denotes the field name. It gets sanitized before being appended to `buf`. /// /// `write_value` is invoked with `buf` as argument to append the value data to `buf`. It must /// not delete from `buf`, but may append arbitrary data. This function then determines the length /// of the data written and adds it in the appropriate place in `buf`. fn put_field_length_encoded(buf: &mut Vec, name: &str, write_value: impl FnOnce(&mut Vec)) { sanitize_name(name, buf); buf.push(b'\n'); buf.extend_from_slice(&[0; 8]); // Length tag, to be populated let start = buf.len(); write_value(buf); let end = buf.len(); buf[start - 8..start].copy_from_slice(&((end - start) as u64).to_le_bytes()); buf.push(b'\n'); } /// Mangle a name into journald-compliant form fn sanitize_name(name: &str, buf: &mut Vec) { buf.extend( name.bytes() .map(|c| if c == b'.' { b'_' } else { c }) .skip_while(|&c| c == b'_') .filter(|&c| c == b'_' || char::from(c).is_ascii_alphanumeric()) .map(|c| char::from(c).to_ascii_uppercase() as u8), ); } /// Append arbitrary data with a well-formed name and value. /// /// `value` must not contain an internal newline, because this function writes /// `value` in the new-line separated format. /// /// For a "newline-safe" variant, see `put_field_length_encoded`. fn put_field_wellformed(buf: &mut Vec, name: &str, value: &[u8]) { buf.extend_from_slice(name.as_bytes()); buf.push(b'\n'); put_value(buf, value); } /// Write the value portion of a key-value pair, in newline separated format. /// /// `value` must not contain an internal newline. /// /// For a "newline-safe" variant, see `put_field_length_encoded`. fn put_value(buf: &mut Vec, value: &[u8]) { buf.extend_from_slice(&(value.len() as u64).to_le_bytes()); buf.extend_from_slice(value); buf.push(b'\n'); } tracing-journald-0.3.0/src/memfd.rs000064400000000000000000000023640072674642500153510ustar 00000000000000//! memfd helpers. use libc::*; use std::fs::File; use std::io::Error; use std::io::Result; use std::os::raw::c_uint; use std::os::unix::prelude::{FromRawFd, RawFd}; fn create(flags: c_uint) -> Result { let fd = memfd_create_syscall(flags); if fd < 0 { Err(Error::last_os_error()) } else { Ok(unsafe { File::from_raw_fd(fd as RawFd) }) } } /// Make the `memfd_create` syscall ourself instead of going through `libc`; /// `memfd_create` isn't supported on `glibc<2.27` so this allows us to /// support old-but-still-used distros like Ubuntu Xenial, Debian Stretch, /// RHEL 7, etc. /// /// See: https://github.com/tokio-rs/tracing/issues/1879 fn memfd_create_syscall(flags: c_uint) -> c_int { unsafe { syscall( SYS_memfd_create, "tracing-journald\0".as_ptr() as *const c_char, flags, ) as c_int } } pub fn create_sealable() -> Result { create(MFD_ALLOW_SEALING | MFD_CLOEXEC) } pub fn seal_fully(fd: RawFd) -> Result<()> { let all_seals = F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE | F_SEAL_SEAL; let result = unsafe { fcntl(fd, F_ADD_SEALS, all_seals) }; if result < 0 { Err(Error::last_os_error()) } else { Ok(()) } } tracing-journald-0.3.0/src/socket.rs000064400000000000000000000050060072674642500155450ustar 00000000000000//! socket helpers. use std::io::{Error, Result}; use std::mem::{size_of, zeroed}; use std::os::unix::ffi::OsStrExt; use std::os::unix::net::UnixDatagram; use std::os::unix::prelude::{AsRawFd, RawFd}; use std::path::Path; use std::ptr; use libc::*; const CMSG_BUFSIZE: usize = 64; #[repr(C)] union AlignedBuffer { buffer: T, align: cmsghdr, } fn assert_cmsg_bufsize() { let space_one_fd = unsafe { CMSG_SPACE(size_of::() as u32) }; assert!( space_one_fd <= CMSG_BUFSIZE as u32, "cmsghdr buffer too small (< {}) to hold a single fd", space_one_fd ); } #[cfg(test)] #[test] fn cmsg_buffer_size_for_one_fd() { assert_cmsg_bufsize() } pub fn send_one_fd_to>(socket: &UnixDatagram, fd: RawFd, path: P) -> Result { assert_cmsg_bufsize(); let mut addr: sockaddr_un = unsafe { zeroed() }; let path_bytes = path.as_ref().as_os_str().as_bytes(); // path_bytes may have at most sun_path + 1 bytes, to account for the trailing NUL byte. if addr.sun_path.len() <= path_bytes.len() { return Err(Error::from_raw_os_error(ENAMETOOLONG)); } addr.sun_family = AF_UNIX as _; unsafe { std::ptr::copy_nonoverlapping( path_bytes.as_ptr(), addr.sun_path.as_mut_ptr() as *mut u8, path_bytes.len(), ) }; let mut msg: msghdr = unsafe { zeroed() }; // Set the target address. msg.msg_name = &mut addr as *mut _ as *mut c_void; msg.msg_namelen = size_of::() as socklen_t; // We send no data body with this message. msg.msg_iov = ptr::null_mut(); msg.msg_iovlen = 0; // Create and fill the control message buffer with our file descriptor let mut cmsg_buffer = AlignedBuffer { buffer: ([0u8; CMSG_BUFSIZE]), }; msg.msg_control = unsafe { cmsg_buffer.buffer.as_mut_ptr() as _ }; msg.msg_controllen = unsafe { CMSG_SPACE(size_of::() as _) as _ }; let mut cmsg: &mut cmsghdr = unsafe { CMSG_FIRSTHDR(&msg).as_mut() }.expect("Control message buffer exhausted"); cmsg.cmsg_level = SOL_SOCKET; cmsg.cmsg_type = SCM_RIGHTS; cmsg.cmsg_len = unsafe { CMSG_LEN(size_of::() as _) as _ }; unsafe { ptr::write(CMSG_DATA(cmsg) as *mut RawFd, fd) }; let result = unsafe { sendmsg(socket.as_raw_fd(), &msg, libc::MSG_NOSIGNAL) }; if result < 0 { Err(Error::last_os_error()) } else { // sendmsg returns the number of bytes written Ok(result as usize) } } tracing-journald-0.3.0/tests/journal.rs000064400000000000000000000225420072674642500163060ustar 00000000000000#![cfg(target_os = "linux")] use std::collections::HashMap; use std::process::Command; use std::time::Duration; use serde::Deserialize; use tracing::{debug, error, info, info_span, warn}; use tracing_journald::Layer; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::Registry; fn journalctl_version() -> std::io::Result { let output = Command::new("journalctl").arg("--version").output()?; Ok(String::from_utf8_lossy(&output.stdout).to_string()) } fn with_journald(f: impl FnOnce()) { with_journald_layer(Layer::new().unwrap().with_field_prefix(None), f) } fn with_journald_layer(layer: Layer, f: impl FnOnce()) { match journalctl_version() { Ok(_) => { let sub = Registry::default().with(layer); tracing::subscriber::with_default(sub, f); } Err(error) => eprintln!( "SKIPPING TEST: journalctl --version failed with error: {}", error ), } } #[derive(Debug, PartialEq, Deserialize)] #[serde(untagged)] enum Field { Text(String), Array(Vec), Binary(Vec), } impl Field { fn as_array(&self) -> Option<&[String]> { match self { Field::Text(_) => None, Field::Binary(_) => None, Field::Array(v) => Some(v), } } fn as_text(&self) -> Option<&str> { match self { Field::Text(v) => Some(v.as_str()), Field::Binary(_) => None, Field::Array(_) => None, } } } // Convenience impls to compare fields against strings and bytes with assert_eq! impl PartialEq<&str> for Field { fn eq(&self, other: &&str) -> bool { match self { Field::Text(s) => s == other, Field::Binary(_) => false, Field::Array(_) => false, } } } impl PartialEq<[u8]> for Field { fn eq(&self, other: &[u8]) -> bool { match self { Field::Text(s) => s.as_bytes() == other, Field::Binary(data) => data == other, Field::Array(_) => false, } } } impl PartialEq> for Field { fn eq(&self, other: &Vec<&str>) -> bool { match self { Field::Text(_) => false, Field::Binary(_) => false, Field::Array(data) => data == other, } } } /// Retry `f` 30 times 100ms apart, i.e. a total of three seconds. /// /// When `f` returns an error wait 100ms and try it again, up to ten times. /// If the last attempt failed return the error returned by that attempt. /// /// If `f` returns Ok immediately return the result. fn retry(f: impl Fn() -> Result) -> Result { let attempts = 30; let interval = Duration::from_millis(100); for attempt in (0..attempts).rev() { match f() { Ok(result) => return Ok(result), Err(e) if attempt == 0 => return Err(e), Err(_) => std::thread::sleep(interval), } } unreachable!() } /// Read from journal with `journalctl`. /// /// `test_name` is a string to match in the `TEST_NAME` field /// of the `journalctl` call, to make sure to only select journal entries /// originating from and relevant to the current test. /// /// Additionally filter by the `_PID` field with the PID of this /// test process, to make sure this method only reads journal entries /// created by this test process. fn read_from_journal(test_name: &str) -> Vec> { let stdout = String::from_utf8( Command::new("journalctl") // We pass --all to circumvent journalctl's default limit of 4096 bytes for field values .args(&["--user", "--output=json", "--all"]) // Filter by the PID of the current test process .arg(format!("_PID={}", std::process::id())) .arg(format!("TEST_NAME={}", test_name)) .output() .unwrap() .stdout, ) .unwrap(); stdout .lines() .map(|l| serde_json::from_str(l).unwrap()) .collect() } /// Read exactly one line from journal for the given test name. /// /// Try to read lines for `testname` from journal, and `retry()` if the wasn't /// _exactly_ one matching line. fn retry_read_one_line_from_journal(testname: &str) -> HashMap { retry(|| { let mut messages = read_from_journal(testname); if messages.len() == 1 { Ok(messages.pop().unwrap()) } else { Err(format!( "one messages expected, got {} messages", messages.len() )) } }) .unwrap() } #[test] fn simple_message() { with_journald(|| { info!(test.name = "simple_message", "Hello World"); let message = retry_read_one_line_from_journal("simple_message"); assert_eq!(message["MESSAGE"], "Hello World"); assert_eq!(message["PRIORITY"], "5"); }); } #[test] fn multiline_message() { with_journald(|| { warn!(test.name = "multiline_message", "Hello\nMultiline\nWorld"); let message = retry_read_one_line_from_journal("multiline_message"); assert_eq!(message["MESSAGE"], "Hello\nMultiline\nWorld"); assert_eq!(message["PRIORITY"], "4"); }); } #[test] fn multiline_message_trailing_newline() { with_journald(|| { error!( test.name = "multiline_message_trailing_newline", "A trailing newline\n" ); let message = retry_read_one_line_from_journal("multiline_message_trailing_newline"); assert_eq!(message["MESSAGE"], "A trailing newline\n"); assert_eq!(message["PRIORITY"], "3"); }); } #[test] fn internal_null_byte() { with_journald(|| { debug!(test.name = "internal_null_byte", "An internal\x00byte"); let message = retry_read_one_line_from_journal("internal_null_byte"); assert_eq!(message["MESSAGE"], b"An internal\x00byte"[..]); assert_eq!(message["PRIORITY"], "6"); }); } #[test] fn large_message() { let large_string = "b".repeat(512_000); with_journald(|| { debug!(test.name = "large_message", "Message: {}", large_string); let message = retry_read_one_line_from_journal("large_message"); assert_eq!( message["MESSAGE"], format!("Message: {}", large_string).as_str() ); assert_eq!(message["PRIORITY"], "6"); }); } #[test] fn simple_metadata() { let sub = Layer::new() .unwrap() .with_field_prefix(None) .with_syslog_identifier("test_ident".to_string()); with_journald_layer(sub, || { info!(test.name = "simple_metadata", "Hello World"); let message = retry_read_one_line_from_journal("simple_metadata"); assert_eq!(message["MESSAGE"], "Hello World"); assert_eq!(message["PRIORITY"], "5"); assert_eq!(message["TARGET"], "journal"); assert_eq!(message["SYSLOG_IDENTIFIER"], "test_ident"); assert!(message["CODE_FILE"].as_text().is_some()); assert!(message["CODE_LINE"].as_text().is_some()); }); } #[test] fn span_metadata() { with_journald(|| { let s1 = info_span!("span1", span_field1 = "foo1"); let _g1 = s1.enter(); info!(test.name = "span_metadata", "Hello World"); let message = retry_read_one_line_from_journal("span_metadata"); assert_eq!(message["MESSAGE"], "Hello World"); assert_eq!(message["PRIORITY"], "5"); assert_eq!(message["TARGET"], "journal"); assert_eq!(message["SPAN_FIELD1"].as_text(), Some("foo1")); assert_eq!(message["SPAN_NAME"].as_text(), Some("span1")); assert!(message["CODE_FILE"].as_text().is_some()); assert!(message["CODE_LINE"].as_text().is_some()); assert!(message["SPAN_CODE_FILE"].as_text().is_some()); assert!(message["SPAN_CODE_LINE"].as_text().is_some()); }); } #[test] fn multiple_spans_metadata() { with_journald(|| { let s1 = info_span!("span1", span_field1 = "foo1"); let _g1 = s1.enter(); let s2 = info_span!("span2", span_field1 = "foo2"); let _g2 = s2.enter(); info!(test.name = "multiple_spans_metadata", "Hello World"); let message = retry_read_one_line_from_journal("multiple_spans_metadata"); assert_eq!(message["MESSAGE"], "Hello World"); assert_eq!(message["PRIORITY"], "5"); assert_eq!(message["TARGET"], "journal"); assert_eq!(message["SPAN_FIELD1"], vec!["foo1", "foo2"]); assert_eq!(message["SPAN_NAME"], vec!["span1", "span2"]); assert!(message["CODE_FILE"].as_text().is_some()); assert!(message["CODE_LINE"].as_text().is_some()); assert!(message.contains_key("SPAN_CODE_FILE")); assert_eq!(message["SPAN_CODE_LINE"].as_array().unwrap().len(), 2); }); } #[test] fn spans_field_collision() { with_journald(|| { let s1 = info_span!("span1", span_field = "foo1"); let _g1 = s1.enter(); let s2 = info_span!("span2", span_field = "foo2"); let _g2 = s2.enter(); info!( test.name = "spans_field_collision", span_field = "foo3", "Hello World" ); let message = retry_read_one_line_from_journal("spans_field_collision"); assert_eq!(message["MESSAGE"], "Hello World"); assert_eq!(message["SPAN_NAME"], vec!["span1", "span2"]); assert_eq!(message["SPAN_FIELD"], vec!["foo1", "foo2", "foo3"]); }); }