use std::io::Write;
#[cfg(unix)]
use std::os::unix::net::UnixDatagram;
#[cfg(not(unix))]
#[allow(dead_code)]
#[derive(Debug)]
pub struct UnixDatagram;
#[cfg(not(unix))]
#[allow(dead_code)]
impl UnixDatagram {
pub fn send(&self, _: &[u8]) -> std::io::Result<usize> {
Ok(0)
}
}
#[derive(Debug)]
#[allow(variant_size_differences)]
pub enum PlatformSink {
Stdout,
File(std::fs::File),
OsLog,
Journald(Option<UnixDatagram>),
}
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
mod syslog_ffi {
use std::ffi::CString;
use std::os::raw::{c_char, c_int};
use std::sync::OnceLock;
pub(super) const LOG_CRIT: c_int = 2;
pub(super) const LOG_ERR: c_int = 3;
pub(super) const LOG_WARNING: c_int = 4;
pub(super) const LOG_NOTICE: c_int = 5;
pub(super) const LOG_INFO: c_int = 6;
pub(super) const LOG_DEBUG: c_int = 7;
const LOG_USER: c_int = 1 << 3;
const LOG_PID: c_int = 0x01;
unsafe extern "C" {
fn openlog(
ident: *const c_char,
logopt: c_int,
facility: c_int,
);
fn syslog(
priority: c_int,
format: *const c_char,
arg: *const c_char,
);
}
static INIT: OnceLock<()> = OnceLock::new();
fn ensure_open() {
INIT.get_or_init(|| {
let ident: &'static CString =
Box::leak(Box::new(CString::new("rlg").unwrap()));
unsafe { openlog(ident.as_ptr(), LOG_PID, LOG_USER) };
});
}
pub(super) unsafe fn emit(priority: c_int, msg: *const c_char) {
ensure_open();
unsafe { syslog(priority, c"%s".as_ptr(), msg) };
}
}
impl PlatformSink {
#[must_use]
pub fn from_config(config: &crate::config::Config) -> Self {
for dest in &config.logging_destinations {
match dest {
crate::config::LoggingDestination::File(path) => {
if let Ok(file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
{
return Self::File(file);
}
}
crate::config::LoggingDestination::Stdout => {
return Self::Stdout;
}
crate::config::LoggingDestination::Network(_) => {
}
}
}
Self::native()
}
#[must_use]
#[allow(clippy::missing_const_for_fn)]
pub fn native() -> Self {
if std::env::var("RLG_FALLBACK_STDOUT").is_ok()
|| std::env::var("GITHUB_ACTIONS").is_ok()
{
return Self::Stdout;
}
#[cfg(target_os = "macos")]
{
Self::OsLog
}
#[cfg(target_os = "linux")]
{
Self::detect_journald()
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
Self::Stdout
}
}
#[cfg(target_os = "linux")]
fn detect_journald() -> Self {
Self::try_journald_socket("/run/systemd/journal/socket")
}
#[cfg(target_os = "linux")]
fn try_journald_socket(path: &str) -> Self {
UnixDatagram::unbound()
.ok()
.and_then(|socket| {
socket.connect(path).ok().map(|()| socket)
})
.map_or(Self::Journald(None), |s| Self::Journald(Some(s)))
}
#[allow(unused_variables)]
pub fn emit(&mut self, level: &str, payload: &[u8]) {
match self {
Self::Stdout => {
let _ = std::io::stdout().write_all(payload);
let _ = std::io::stdout().write_all(b"\n");
}
Self::File(f) => {
let _ = f.write_all(payload);
let _ = f.write_all(b"\n");
}
Self::OsLog => Self::emit_os_log(level, payload),
Self::Journald(socket_opt) => {
Self::emit_journald(
level,
payload,
socket_opt.as_ref(),
);
}
}
}
#[cfg(target_os = "macos")]
fn emit_os_log(level: &str, payload: &[u8]) {
use syslog_ffi::{
LOG_CRIT, LOG_DEBUG, LOG_ERR, LOG_INFO, LOG_NOTICE,
LOG_WARNING, emit,
};
if std::env::var("RLG_FALLBACK_STDOUT").is_ok()
|| std::env::var("GITHUB_ACTIONS").is_ok()
{
let _ = std::io::stdout().write_all(payload);
let _ = std::io::stdout().write_all(b"\n");
return;
}
let priority = match level {
"FATAL" | "CRITICAL" => LOG_CRIT,
"ERROR" => LOG_ERR,
"WARN" => LOG_WARNING,
"INFO" => LOG_INFO,
"DEBUG" | "TRACE" | "VERBOSE" => LOG_DEBUG,
_ => LOG_NOTICE,
};
let mut buf: Vec<u8> =
payload.iter().copied().filter(|&b| b != 0).collect();
buf.push(0);
#[allow(unsafe_code)]
unsafe {
emit(priority, buf.as_ptr().cast::<std::os::raw::c_char>());
}
}
#[cfg(not(target_os = "macos"))]
const fn emit_os_log(_level: &str, _payload: &[u8]) {}
fn emit_journald(
level: &str,
payload: &[u8],
socket_opt: Option<&UnixDatagram>,
) {
let Some(socket) = socket_opt else {
let _ = std::io::stdout().write_all(payload);
let _ = std::io::stdout().write_all(b"\n");
return;
};
if std::env::var("RLG_FALLBACK_STDOUT").is_ok()
|| std::env::var("GITHUB_ACTIONS").is_ok()
{
let _ = socket;
return;
}
#[cfg(target_os = "linux")]
{
let priority = match level {
"ERROR" | "FATAL" | "CRITICAL" => "3",
"WARN" => "4",
"INFO" => "6",
"DEBUG" | "TRACE" | "VERBOSE" => "7",
_ => "5",
};
let mut journal_payload =
Vec::with_capacity(payload.len() + 32);
journal_payload.extend_from_slice(b"PRIORITY=");
journal_payload.extend_from_slice(priority.as_bytes());
journal_payload.extend_from_slice(b"\nMESSAGE=");
journal_payload.extend_from_slice(payload);
journal_payload.extend_from_slice(b"\n");
let _ = socket.send(&journal_payload);
}
#[cfg(not(target_os = "linux"))]
{
let _ = (level, payload, socket);
}
}
}
#[cfg(all(test, not(miri)))]
mod tests {
use super::*;
use serial_test::serial;
#[test]
#[cfg_attr(miri, ignore)]
fn test_platform_sink_stdout() {
let mut sink = PlatformSink::Stdout;
sink.emit("INFO", b"test stdout");
}
#[test]
#[cfg_attr(miri, ignore)]
#[allow(unsafe_code)]
#[serial]
fn test_platform_sink_fallback_env_var() {
unsafe { std::env::set_var("RLG_FALLBACK_STDOUT", "1") };
let sink = PlatformSink::native();
assert!(matches!(sink, PlatformSink::Stdout));
unsafe { std::env::remove_var("RLG_FALLBACK_STDOUT") };
}
#[test]
#[cfg_attr(miri, ignore)]
#[allow(unsafe_code)]
#[serial]
fn test_platform_sink_native_journald_path() {
unsafe {
std::env::remove_var("RLG_FALLBACK_STDOUT");
std::env::remove_var("GITHUB_ACTIONS");
}
let sink = PlatformSink::native();
#[cfg(target_os = "linux")]
assert!(matches!(sink, PlatformSink::Journald(_)));
#[cfg(target_os = "macos")]
assert!(matches!(sink, PlatformSink::OsLog));
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
assert!(matches!(sink, PlatformSink::Stdout));
unsafe { std::env::set_var("RLG_FALLBACK_STDOUT", "1") };
}
#[test]
#[cfg_attr(miri, ignore)]
#[cfg(target_os = "linux")]
fn test_try_journald_socket_failure() {
let sink =
PlatformSink::try_journald_socket("/nonexistent/path");
assert!(matches!(sink, PlatformSink::Journald(None)));
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_platform_sink_journald_coverage() {
#[cfg(unix)]
{
let (sock1, _sock2) = UnixDatagram::pair().unwrap();
let mut sink = PlatformSink::Journald(Some(sock1));
sink.emit("INFO", b"test journald");
}
let mut sink_none = PlatformSink::Journald(None);
sink_none.emit("INFO", b"test journald fallback");
}
#[test]
#[cfg_attr(miri, ignore)]
#[allow(unsafe_code)]
#[serial]
fn test_platform_sink_oslog_fallback_stdout() {
unsafe { std::env::set_var("RLG_FALLBACK_STDOUT", "1") };
let mut sink = PlatformSink::OsLog;
sink.emit("INFO", b"fallback-test");
unsafe { std::env::remove_var("RLG_FALLBACK_STDOUT") };
}
#[cfg(target_os = "macos")]
#[test]
#[cfg_attr(miri, ignore)]
#[allow(unsafe_code)]
#[serial]
fn test_platform_sink_oslog_real_syslog_call() {
unsafe {
std::env::remove_var("RLG_FALLBACK_STDOUT");
std::env::remove_var("GITHUB_ACTIONS");
}
let mut sink = PlatformSink::OsLog;
for level in [
"FATAL",
"CRITICAL",
"ERROR",
"WARN",
"INFO",
"DEBUG",
"TRACE",
"VERBOSE",
"UNKNOWN_LEVEL",
] {
sink.emit(level, b"rlg coverage test record");
}
unsafe { std::env::set_var("RLG_FALLBACK_STDOUT", "1") };
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_platform_sink_oslog_with_embedded_nulls() {
let mut sink = PlatformSink::OsLog;
sink.emit("INFO", b"with\0embedded\0nulls");
}
}