From cecdcfb964ca33ee4d51be8939e4ce2899f6ca94 Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 14 Nov 2024 09:55:25 +0800 Subject: [PATCH 1/7] feat: support journald appender Signed-off-by: tison --- Cargo.toml | 2 + src/append/journald/README.md | 3 + src/append/journald/field.rs | 195 +++++++++++++++++++++ src/append/journald/memfd.rs | 163 ++++++++++++++++++ src/append/journald/mod.rs | 310 ++++++++++++++++++++++++++++++++++ src/append/mod.rs | 4 + src/append/syslog.rs | 4 +- 7 files changed, 679 insertions(+), 2 deletions(-) create mode 100644 src/append/journald/README.md create mode 100644 src/append/journald/field.rs create mode 100644 src/append/journald/memfd.rs create mode 100644 src/append/journald/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 6a3e4b7..983aa78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ rustdoc-args = ["--cfg", "docsrs"] [features] fastrace = ["dep:fastrace"] +journald = ["dep:libc"] json = ["dep:serde_json", "dep:serde", "jiff/serde"] no-color = ["colored/no-color"] non-blocking = ["dep:crossbeam-channel"] @@ -55,6 +56,7 @@ log = { version = "0.4", features = ["std", "kv_unstable"] } crossbeam-channel = { version = "0.5", optional = true } fastrace = { version = "0.7", optional = true } fasyslog = { version = "0.2", optional = true } +libc = { version = "0.2.162", optional = true } opentelemetry = { version = "0.27", features = ["logs"], optional = true } opentelemetry-otlp = { version = "0.27", features = [ "logs", diff --git a/src/append/journald/README.md b/src/append/journald/README.md new file mode 100644 index 0000000..f12cdb1 --- /dev/null +++ b/src/append/journald/README.md @@ -0,0 +1,3 @@ +# Rolling File Appender + +This appender is a remix of [tracing-journald](https://crates.io/crates/tracing-journald) and [systemd-journal-logger](https://crates.io/crates/systemd-journal-logger), with several modifications to fit this crate's needs. diff --git a/src/append/journald/field.rs b/src/append/journald/field.rs new file mode 100644 index 0000000..fcd8d69 --- /dev/null +++ b/src/append/journald/field.rs @@ -0,0 +1,195 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This field is derived from https://github.com/swsnr/systemd-journal-logger.rs/blob/v2.2.0/src/fields.rs. + +//! Write well-formated journal fields to buffers. + +use std::fmt::Arguments; +use std::io::Write; + +use log::kv::Value; + +pub(super) enum FieldName<'a> { + WellFormed(&'a str), + WriteEscaped(&'a str), +} + +/// Whether `c` is a valid character in the key of a journal field. +/// +/// Journal field keys may only contain ASCII uppercase letters A to Z, +/// numbers 0 to 9 and the underscore. +fn is_valid_key_char(c: char) -> bool { + matches!(c, 'A'..='Z' | '0'..='9' | '_') +} + +/// Write an escaped `key` for use in a systemd journal field. +/// +/// See [`super::Journald`] for the rules. +fn write_escaped_key(key: &str, buffer: &mut Vec) { + // Key length is limited to 64 bytes + let mut remaining = 64; + + let escaped = key + .to_ascii_uppercase() + .replace(|c| !is_valid_key_char(c), "_"); + + if escaped.starts_with(|c: char| matches!(c, '_' | '0'..='9')) { + buffer.extend_from_slice(b"ESCAPED_"); + remaining -= 8; + } + + for b in escaped.into_bytes() { + if remaining == 0 { + break; + } + buffer.push(b); + remaining -= 1; + } +} + +fn put_field_name(buffer: &mut Vec, name: FieldName<'_>) { + match name { + FieldName::WellFormed(name) => buffer.extend_from_slice(name.as_bytes()), + FieldName::WriteEscaped("") => buffer.extend_from_slice(b"EMPTY"), + FieldName::WriteEscaped(name) => write_escaped_key(name, buffer), + } +} + +pub(super) trait PutAsFieldValue { + fn put_field_value(self, buffer: &mut Vec); +} + +impl PutAsFieldValue for &[u8] { + fn put_field_value(self, buffer: &mut Vec) { + buffer.extend_from_slice(self) + } +} + +impl PutAsFieldValue for &Arguments<'_> { + fn put_field_value(self, buffer: &mut Vec) { + match self.as_str() { + Some(s) => buffer.extend_from_slice(s.as_bytes()), + None => { + // SAFETY: no more than an allocate-less version + // buffer.extend_from_slice(format!("{}", self)) + write!(buffer, "{}", self).unwrap() + } + } + } +} + +impl PutAsFieldValue for Value<'_> { + fn put_field_value(self, buffer: &mut Vec) { + // SAFETY: no more than an allocate-less version + // buffer.extend_from_slice(format!("{}", self)) + write!(buffer, "{}", self).unwrap(); + } +} + +pub(super) fn put_field_length_encoded( + buffer: &mut Vec, + name: FieldName<'_>, + value: V, +) { + put_field_name(buffer, name); + buffer.push(b'\n'); + // Reserve the length tag + buffer.extend_from_slice(&[0; 8]); + let value_start = buffer.len(); + value.put_field_value(buffer); + let value_end = buffer.len(); + // Fill the length tag + let length_bytes = ((value_end - value_start) as u64).to_le_bytes(); + buffer[value_start - 8..value_start].copy_from_slice(&length_bytes); + buffer.push(b'\n'); +} + +pub(super) fn put_field_bytes(buffer: &mut Vec, name: FieldName<'_>, value: &[u8]) { + if value.contains(&b'\n') { + // Write as length encoded field + put_field_length_encoded(buffer, name, value); + } else { + put_field_name(buffer, name); + buffer.push(b'='); + buffer.extend_from_slice(value); + buffer.push(b'\n'); + } +} + +#[cfg(test)] +mod tests { + use FieldName::*; + + use super::*; + + #[test] + fn test_escape_journal_key() { + for case in ["FOO", "FOO_123"] { + let mut bs = vec![]; + write_escaped_key(case, &mut bs); + assert_eq!(String::from_utf8_lossy(&bs), case); + } + + let cases = vec![ + ("foo", "FOO"), + ("_foo", "ESCAPED__FOO"), + ("1foo", "ESCAPED_1FOO"), + ("Hallöchen", "HALL_CHEN"), + ]; + for (key, expected) in cases { + let mut bs = vec![]; + write_escaped_key(key, &mut bs); + assert_eq!(String::from_utf8_lossy(&bs), expected); + } + + { + for case in [ + "very_long_key_name_that_is_longer_than_64_bytes".repeat(5), + "_need_escape_very_long_key_name_that_is_longer_than_64_bytes".repeat(5), + ] { + let mut bs = vec![]; + write_escaped_key(&case, &mut bs); + println!("{:?}", String::from_utf8_lossy(&bs)); + assert_eq!(bs.len(), 64); + } + } + } + + #[test] + fn test_put_field_length_encoded() { + let mut buffer = Vec::new(); + // See "Data Format" in https://systemd.io/JOURNAL_NATIVE_PROTOCOL/ for this example + put_field_length_encoded(&mut buffer, WellFormed("FOO"), "BAR".as_bytes()); + assert_eq!(&buffer, b"FOO\n\x03\0\0\0\0\0\0\0BAR\n"); + } + + #[test] + fn test_put_field_bytes_no_newline() { + let mut buffer = Vec::new(); + put_field_bytes(&mut buffer, WellFormed("FOO"), "BAR".as_bytes()); + assert_eq!(&buffer, b"FOO=BAR\n"); + } + + #[test] + fn test_put_field_bytes_newline() { + let mut buffer = Vec::new(); + put_field_bytes( + &mut buffer, + WellFormed("FOO"), + "BAR\nSPAM_WITH_EGGS".as_bytes(), + ); + assert_eq!(&buffer, b"FOO\n\x12\0\0\0\0\0\0\0BAR\nSPAM_WITH_EGGS\n"); + } +} diff --git a/src/append/journald/memfd.rs b/src/append/journald/memfd.rs new file mode 100644 index 0000000..eebca80 --- /dev/null +++ b/src/append/journald/memfd.rs @@ -0,0 +1,163 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Large journal entries are sent over the socket with an empty payload, but with a single memfd +//! file descriptor that contains the literal journal entry data. See also +//! [JOURNAL_NATIVE_PROTOCOL]. +//! +//! [JOURNAL_NATIVE_PROTOCOL]: https://systemd.io/JOURNAL_NATIVE_PROTOCOL/ + +use std::fs::File; +use std::io; +use std::io::Write; +use std::mem; +use std::os::fd::AsRawFd; +use std::os::fd::FromRawFd; +use std::os::fd::RawFd; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::net::UnixDatagram; +use std::path::Path; +use std::ptr; + +fn send_large_payload(socket: &UnixDatagram, 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 = 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. + seal_fully(mem.as_raw_fd())?; + send_one_fd_to(socket, mem.as_raw_fd(), super::JOURNALD_PATH) +} + +fn create(flags: libc::c_uint) -> io::Result { + let fd = memfd_create_syscall(flags); + if fd < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(unsafe { File::from_raw_fd(fd as RawFd) }) + } +} + +/// Make the `memfd_create` syscall ourselves 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 16.04, Debian Stretch, +/// RHEL 7, etc. +/// +/// See: https://github.com/tokio-rs/tracing/issues/1879 +fn memfd_create_syscall(flags: libc::c_uint) -> libc::c_int { + unsafe { + libc::syscall( + libc::SYS_memfd_create, + "logforth-journald\0".as_ptr() as *const libc::c_char, + flags, + ) as libc::c_int + } +} + +fn create_sealable() -> io::Result { + create(libc::MFD_ALLOW_SEALING | libc::MFD_CLOEXEC) +} + +fn seal_fully(fd: RawFd) -> io::Result<()> { + let all_seals = + libc::F_SEAL_SHRINK | libc::F_SEAL_GROW | libc::F_SEAL_WRITE | libc::F_SEAL_SEAL; + let result = unsafe { libc::fcntl(fd, libc::F_ADD_SEALS, all_seals) }; + if result < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } +} + +const CMSG_BUFSIZE: usize = 64; + +#[repr(C)] +union AlignedBuffer { + buffer: T, + align: libc::cmsghdr, +} + +fn assert_cmsg_bufsize() { + let space_one_fd = unsafe { libc::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 + ); +} + +fn send_one_fd_to>(socket: &UnixDatagram, fd: RawFd, path: P) -> io::Result { + assert_cmsg_bufsize(); + + let mut addr: libc::sockaddr_un = unsafe { mem::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(io::Error::from_raw_os_error(libc::ENAMETOOLONG)); + } + + addr.sun_family = libc::AF_UNIX as _; + unsafe { + ptr::copy_nonoverlapping( + path_bytes.as_ptr(), + addr.sun_path.as_mut_ptr() as *mut u8, + path_bytes.len(), + ) + }; + + let mut msg: libc::msghdr = unsafe { mem::zeroed() }; + // Set the target address. + msg.msg_name = &mut addr as *mut _ as *mut libc::c_void; + msg.msg_namelen = size_of::() as libc::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 { libc::CMSG_SPACE(size_of::() as _) as _ }; + + let cmsg: &mut libc::cmsghdr = + unsafe { libc::CMSG_FIRSTHDR(&msg).as_mut() }.expect("Control message buffer exhausted"); + + cmsg.cmsg_level = libc::SOL_SOCKET; + cmsg.cmsg_type = libc::SCM_RIGHTS; + cmsg.cmsg_len = unsafe { libc::CMSG_LEN(size_of::() as _) as _ }; + + unsafe { ptr::write(libc::CMSG_DATA(cmsg) as *mut RawFd, fd) }; + + let result = unsafe { libc::sendmsg(socket.as_raw_fd(), &msg, libc::MSG_NOSIGNAL) }; + + if result < 0 { + Err(io::Error::last_os_error()) + } else { + // sendmsg returns the number of bytes written + Ok(result as usize) + } +} + +#[cfg(test)] +mod tests { + #[test] + fn cmsg_buffer_size_for_one_fd() { + super::assert_cmsg_bufsize() + } +} diff --git a/src/append/journald/mod.rs b/src/append/journald/mod.rs new file mode 100644 index 0000000..63add72 --- /dev/null +++ b/src/append/journald/mod.rs @@ -0,0 +1,310 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io; +use std::io::Write; +use std::os::unix::net::UnixDatagram; + +use log::Level; +use log::Record; + +use crate::Append; + +mod field; +#[cfg(target_os = "linux")] +mod memfd; + +const JOURNALD_PATH: &str = "/run/systemd/journal/socket"; + +fn current_exe_identifier() -> Option { + let executable = std::env::current_exe().ok()?; + Some(executable.file_name()?.to_string_lossy().into_owned()) +} + +/// A systemd journal appender. +/// +/// ## Journal access +/// +/// ## Standard fields +/// +/// The journald appender always sets the following standard [journal fields]: +/// +/// - `PRIORITY`: The log level mapped to a priority (see below). +/// - `MESSAGE`: The formatted log message (see [`Record::args()`]). +/// - `SYSLOG_PID`: The PID of the running process (see [`std::process::id()`]). +/// - `CODE_FILE`: The filename the log message originates from (see [`Record::file()`], only if +/// present). +/// - `CODE_LINE`: The line number the log message originates from (see [`Record::line()`], only if +/// present). +/// +/// It also sets `SYSLOG_IDENTIFIER` if non-empty (see [`Journald::with_syslog_identifier`]). +/// +/// Additionally, it also adds the following non-standard fields: +/// +/// - `TARGET`: The target of the log record (see [`Record::target()`]). +/// - `CODE_MODULE`: The module path of the log record (see [`Record::module_path()`], only if +/// present). +/// +/// [journal fields]: https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html +/// +/// ## Log levels and Priorities +/// +/// [`Level`] gets mapped to journal (syslog) priorities as follows: +/// +/// - [`Level::Error`] → `3` (err) +/// - [`Level::Warn`] → `4` (warning) +/// - [`Level::Info`] → `5` (notice) +/// - [`Level::Debug`] → `6` (info) +/// - [`Level::Trace`] → `7` (debug) +/// +/// Higher priorities (crit, alert, and emerg) are not used. +/// +/// ## Custom fields and structured record fields +/// +/// In addition to these fields the appender also adds all structures key-values +/// (see [`Record::key_values`]) from each log record as journal fields, +/// and also supports global extra fields via [`Journald::with_extra_fields`]. +/// +/// Journald allows only ASCII uppercase letters, ASCII digits, and the +/// underscore in field names, and limits field names to 64 bytes. See +/// [`journal_field_valid`][jfv] for the precise validation rules. +/// +/// This appender mangles the keys of additional key-values on records and names +/// of custom fields according to the following rules, to turn them into valid +/// journal fields: +/// +/// - If the key is entirely empty, use `EMPTY`. +/// - Transform the entire value to ASCII uppercase. +/// - Replace all invalid characters with underscore. +/// - If the key starts with an underscore or digit, which is not permitted, prepend `ESCAPED_`. +/// - Cap the result to 64 bytes. +/// +/// [jfv]: https://github.com/systemd/systemd/blob/v256.7/src/libsystemd/sd-journal/journal-file.c#L1703 +/// +/// # Errors +/// +/// The appender tries to connect to journald when constructed, to provide early +/// on feedback if journald is not available (e.g. in containers where the +/// journald socket is not mounted into the container). +#[derive(Debug)] +pub struct Journald { + /// The datagram socket to send messages to journald. + socket: UnixDatagram, + /// Preformatted extra fields to be appended to every log message. + extra_fields: Vec, + /// The syslog identifier. + syslog_identifier: String, +} + +impl Journald { + /// Construct a journald appender + /// + /// Fails if the journald socket couldn't be opened. + pub fn new() -> io::Result { + let socket = UnixDatagram::unbound()?; + let sub = Self { + socket, + extra_fields: Vec::new(), + syslog_identifier: current_exe_identifier().unwrap_or_default(), + }; + // 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. + sub.send_payload(&[])?; + Ok(sub) + } + + /// Add an extra field to be added to every log entry. + /// + /// `name` is the name of a custom field, and `value` its value. Fields are + /// appended to every log entry, in order they were added to the appender. + /// + /// ## Restrictions on field names + /// + /// `name` should be a valid journal file name, i.e. it must only contain + /// ASCII uppercase alphanumeric characters and the underscore, and must + /// start with an ASCII uppercase letter. + /// + /// Invalid keys in `extra_fields` are escaped according to the rules + /// documented in [`Journald`]. + /// + /// It is not recommended that `name` is any of the standard fields already + /// added by this appender (see [`Journald`]); though journald supports + /// multiple values for a field, journald clients may not handle unexpected + /// multi-value fields properly and perhaps only show the first value. + /// Specifically, even `journalctl` will only show the first `MESSAGE` value + /// of journal entries. + /// + /// ## Restrictions on values + /// + /// There are no restrictions on the value. + pub fn with_extra_field, V: AsRef<[u8]>>(mut self, name: K, value: V) -> Self { + field::put_field_bytes( + &mut self.extra_fields, + field::FieldName::WriteEscaped(name.as_ref()), + value.as_ref(), + ); + self + } + + /// Add extra fields to be added to every log entry. + /// + /// See [`Self::with_extra_field`] for details. + pub fn with_extra_fields(mut self, extra_fields: I) -> Self + where + I: IntoIterator, + K: AsRef, + V: AsRef<[u8]>, + { + for (name, value) in extra_fields { + field::put_field_bytes( + &mut self.extra_fields, + field::FieldName::WriteEscaped(name.as_ref()), + value.as_ref(), + ); + } + self + } + + /// Sets the syslog identifier for this appender. + /// + /// 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 + } + + 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::Other, + "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 { + memfd::send_large_payload(&self.socket, payload) + } +} + +struct WriteKeyValues<'a>(&'a mut Vec); + +impl<'kvs> log::kv::Visitor<'kvs> for WriteKeyValues<'_> { + fn visit_pair( + &mut self, + key: log::kv::Key<'kvs>, + value: log::kv::Value<'kvs>, + ) -> Result<(), log::kv::Error> { + field::put_field_length_encoded( + self.0, + field::FieldName::WriteEscaped(key.as_str()), + value, + ); + Ok(()) + } +} + +impl Append for Journald { + /// Extract all fields (standard and custom) from `record`, append all `extra_fields` given + /// to this appender, and send the result to journald. + fn append(&self, record: &Record) -> anyhow::Result<()> { + use field::*; + + let mut buffer = vec![]; + + // Write standard fields. Numeric fields can't contain new lines so we + // write them directly, everything else goes through the put functions + // for property mangling and length-encoding + let priority = match record.level() { + Level::Error => b"3", + Level::Warn => b"4", + Level::Info => b"5", + Level::Debug => b"6", + Level::Trace => b"7", + }; + + put_field_bytes(&mut buffer, FieldName::WellFormed("PRIORITY"), priority); + put_field_length_encoded(&mut buffer, FieldName::WellFormed("MESSAGE"), record.args()); + // Syslog compatibility fields + writeln!(&mut buffer, "SYSLOG_PID={}", std::process::id())?; + if !self.syslog_identifier.is_empty() { + put_field_bytes( + &mut buffer, + FieldName::WellFormed("SYSLOG_IDENTIFIER"), + self.syslog_identifier.as_bytes(), + ); + } + if let Some(file) = record.file() { + put_field_bytes( + &mut buffer, + FieldName::WellFormed("CODE_FILE"), + file.as_bytes(), + ); + } + if let Some(module) = record.module_path() { + put_field_bytes( + &mut buffer, + FieldName::WellFormed("CODE_MODULE"), + module.as_bytes(), + ); + } + if let Some(line) = record.line() { + writeln!(&mut buffer, "CODE_LINE={}", line)?; + } + put_field_bytes( + &mut buffer, + FieldName::WellFormed("TARGET"), + record.target().as_bytes(), + ); + // Put all structured values of the record + record + .key_values() + .visit(&mut WriteKeyValues(&mut buffer))?; + // Put all extra fields of the appender + buffer.extend_from_slice(&self.extra_fields); + self.send_payload(&buffer)?; + Ok(()) + } +} diff --git a/src/append/mod.rs b/src/append/mod.rs index d8c9ee8..2ade54c 100644 --- a/src/append/mod.rs +++ b/src/append/mod.rs @@ -18,6 +18,8 @@ use std::fmt; #[cfg(feature = "fastrace")] mod fastrace; +#[cfg(all(unix, feature = "journald"))] +mod journald; #[cfg(feature = "opentelemetry")] pub mod opentelemetry; #[cfg(feature = "rolling-file")] @@ -28,6 +30,8 @@ pub mod syslog; #[cfg(feature = "fastrace")] pub use self::fastrace::FastraceEvent; +#[cfg(all(unix, feature = "journald"))] +pub use self::journald::Journald; #[cfg(feature = "opentelemetry")] pub use self::opentelemetry::OpentelemetryLog; #[cfg(feature = "rolling-file")] diff --git a/src/append/syslog.rs b/src/append/syslog.rs index 5929d4c..8fcd398 100644 --- a/src/append/syslog.rs +++ b/src/append/syslog.rs @@ -107,8 +107,8 @@ fn log_level_to_otel_severity(level: log::Level) -> fasyslog::Severity { match level { log::Level::Error => fasyslog::Severity::ERROR, log::Level::Warn => fasyslog::Severity::WARNING, - log::Level::Info => fasyslog::Severity::INFORMATIONAL, - log::Level::Debug => fasyslog::Severity::DEBUG, + log::Level::Info => fasyslog::Severity::NOTICE, + log::Level::Debug => fasyslog::Severity::INFORMATIONAL, log::Level::Trace => fasyslog::Severity::DEBUG, } } From d81cb5923e808cf5ea38767908b3545fb50004ae Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 14 Nov 2024 10:05:39 +0800 Subject: [PATCH 2/7] fix compile Signed-off-by: tison --- src/append/journald/memfd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/append/journald/memfd.rs b/src/append/journald/memfd.rs index eebca80..1c137da 100644 --- a/src/append/journald/memfd.rs +++ b/src/append/journald/memfd.rs @@ -30,7 +30,7 @@ use std::os::unix::net::UnixDatagram; use std::path::Path; use std::ptr; -fn send_large_payload(socket: &UnixDatagram, payload: &[u8]) -> io::Result { +pub(super) fn send_large_payload(socket: &UnixDatagram, 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; From 1138cd399ed97fa3e65d9b5f68c7c8065b373c45 Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 14 Nov 2024 10:08:52 +0800 Subject: [PATCH 3/7] fix check Signed-off-by: tison --- src/append/journald/field.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/append/journald/field.rs b/src/append/journald/field.rs index fcd8d69..fe437cd 100644 --- a/src/append/journald/field.rs +++ b/src/append/journald/field.rs @@ -14,7 +14,7 @@ // This field is derived from https://github.com/swsnr/systemd-journal-logger.rs/blob/v2.2.0/src/fields.rs. -//! Write well-formated journal fields to buffers. +//! Write well-formatted journal fields to buffers. use std::fmt::Arguments; use std::io::Write; From b7e65c759ef75e4b794a6334bfc8f1c08023254b Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 14 Nov 2024 10:12:39 +0800 Subject: [PATCH 4/7] add example Signed-off-by: tison --- Cargo.toml | 6 ++++++ examples/journald.rs | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 examples/journald.rs diff --git a/Cargo.toml b/Cargo.toml index 983aa78..684b69b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,3 +109,9 @@ doc-scrape-examples = true name = "syslog" path = "examples/syslog.rs" required-features = ["syslog"] + +[[example]] +doc-scrape-examples = true +name = "journald" +path = "examples/journald.rs" +required-features = ["journald"] diff --git a/examples/journald.rs b/examples/journald.rs new file mode 100644 index 0000000..7f49f36 --- /dev/null +++ b/examples/journald.rs @@ -0,0 +1,18 @@ +#[cfg(unix)] +fn main() { + use logforth::append::Journald; + + let append = Journald::new().unwrap(); + logforth::builder().dispatch(|d| d.append(append)).apply(); + + log::error!("Hello, journald at ERROR!"); + log::warn!("Hello, journald at WARN!"); + log::info!("Hello, journald at INFO!"); + log::debug!("Hello, journald at DEBUG!"); + log::trace!("Hello, journald at TRACE!"); +} + +#[cfg(not(unix))] +fn main() { + println!("This example is only for Unix-like systems."); +} From 33926c8ab1d3e41feb159e13ab4e3091b4ec5b11 Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 14 Nov 2024 10:14:59 +0800 Subject: [PATCH 5/7] fix compile Signed-off-by: tison --- src/append/journald/memfd.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/append/journald/memfd.rs b/src/append/journald/memfd.rs index 1c137da..d0e1c03 100644 --- a/src/append/journald/memfd.rs +++ b/src/append/journald/memfd.rs @@ -30,10 +30,9 @@ use std::os::unix::net::UnixDatagram; use std::path::Path; use std::ptr; +// If the payload's too large for a single datagram, send it through a memfd, see +// https://systemd.io/JOURNAL_NATIVE_PROTOCOL/ pub(super) fn send_large_payload(socket: &UnixDatagram, 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 = create_sealable()?; mem.write_all(payload)?; @@ -92,7 +91,7 @@ union AlignedBuffer { } fn assert_cmsg_bufsize() { - let space_one_fd = unsafe { libc::CMSG_SPACE(size_of::() as u32) }; + let space_one_fd = unsafe { libc::CMSG_SPACE(mem::size_of::() as u32) }; assert!( space_one_fd <= CMSG_BUFSIZE as u32, "cmsghdr buffer too small (< {}) to hold a single fd", @@ -122,7 +121,7 @@ fn send_one_fd_to>(socket: &UnixDatagram, fd: RawFd, path: P) -> let mut msg: libc::msghdr = unsafe { mem::zeroed() }; // Set the target address. msg.msg_name = &mut addr as *mut _ as *mut libc::c_void; - msg.msg_namelen = size_of::() as libc::socklen_t; + msg.msg_namelen = mem::size_of::() as libc::socklen_t; // We send no data body with this message. msg.msg_iov = ptr::null_mut(); @@ -133,14 +132,14 @@ fn send_one_fd_to>(socket: &UnixDatagram, fd: RawFd, path: P) -> buffer: [0u8; CMSG_BUFSIZE], }; msg.msg_control = unsafe { cmsg_buffer.buffer.as_mut_ptr() as _ }; - msg.msg_controllen = unsafe { libc::CMSG_SPACE(size_of::() as _) as _ }; + msg.msg_controllen = unsafe { libc::CMSG_SPACE(mem::size_of::() as _) as _ }; let cmsg: &mut libc::cmsghdr = unsafe { libc::CMSG_FIRSTHDR(&msg).as_mut() }.expect("Control message buffer exhausted"); cmsg.cmsg_level = libc::SOL_SOCKET; cmsg.cmsg_type = libc::SCM_RIGHTS; - cmsg.cmsg_len = unsafe { libc::CMSG_LEN(size_of::() as _) as _ }; + cmsg.cmsg_len = unsafe { libc::CMSG_LEN(mem::size_of::() as _) as _ }; unsafe { ptr::write(libc::CMSG_DATA(cmsg) as *mut RawFd, fd) }; From 97690a21e260aae27e197e9fb7f3543c69ad4f50 Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 14 Nov 2024 10:18:44 +0800 Subject: [PATCH 6/7] add changelog Signed-off-by: tison --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83844fb..1c7b264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to this project will be documented in this file. +## [0.18.0] 2024-11-14 + +### Breaking changes + +* The mapping between syslog severity and log's level is changed. + * `log::Level::Error` is mapped to `syslog::Severity::Error` (unchanged). + * `log::Level::Warn` is mapped to `syslog::Severity::Warning` (unchanged). + * `log::Level::Info` is mapped to `syslog::Severity::Notice` (changed). + * `log::Level::Debug` is mapped to `syslog::Severity::Info` (changed). + * `log::Level::Trace` is mapped to `syslog::Severity::Debug` (unchanged). + +### New features + +* Add `journald` feature to support journald appenders ([#80](https://github.com/fast/logforth/pull/80)). + ## [0.17.1] 2024-11-12 ### Refactors From aaf7e9e108538a4b73ecb02fe21c1517eb4bdc3d Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 14 Nov 2024 10:21:55 +0800 Subject: [PATCH 7/7] fix check Signed-off-by: tison --- examples/journald.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/examples/journald.rs b/examples/journald.rs index 7f49f36..d6e7173 100644 --- a/examples/journald.rs +++ b/examples/journald.rs @@ -1,3 +1,17 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + #[cfg(unix)] fn main() { use logforth::append::Journald;