1use std::io::Write;
13
14#[cfg(unix)]
15use std::os::unix::net::UnixDatagram;
16
17#[cfg(not(unix))]
18#[allow(dead_code)]
19#[derive(Debug)]
20pub struct UnixDatagram;
21
22#[cfg(not(unix))]
23#[allow(dead_code)]
24impl UnixDatagram {
25 pub fn send(&self, _: &[u8]) -> std::io::Result<usize> {
26 Ok(0)
27 }
28}
29
30#[derive(Debug)]
32#[allow(variant_size_differences)]
33pub enum PlatformSink {
34 Stdout,
36 File(std::fs::File),
38 OsLog,
40 Journald(Option<UnixDatagram>),
42 #[cfg(all(target_os = "linux", feature = "uring"))]
54 #[cfg_attr(
55 docsrs,
56 doc(cfg(all(target_os = "linux", feature = "uring")))
57 )]
58 UringFile(std::fs::File),
59}
60
61#[cfg(target_os = "macos")]
71#[allow(unsafe_code)]
72mod syslog_ffi {
73 use std::ffi::CStr;
74 use std::os::raw::{c_char, c_int};
75 use std::sync::OnceLock;
76
77 pub(super) const LOG_CRIT: c_int = 2;
78 pub(super) const LOG_ERR: c_int = 3;
79 pub(super) const LOG_WARNING: c_int = 4;
80 pub(super) const LOG_NOTICE: c_int = 5;
81 pub(super) const LOG_INFO: c_int = 6;
82 pub(super) const LOG_DEBUG: c_int = 7;
83 const LOG_USER: c_int = 1 << 3;
84 const LOG_PID: c_int = 0x01;
85
86 unsafe extern "C" {
87 fn openlog(
88 ident: *const c_char,
89 logopt: c_int,
90 facility: c_int,
91 );
92 fn syslog(
93 priority: c_int,
94 format: *const c_char,
95 arg: *const c_char,
96 );
97 }
98
99 static INIT: OnceLock<()> = OnceLock::new();
100
101 fn ensure_open() {
102 INIT.get_or_init(|| {
103 const IDENT: &CStr = c"rlg";
108 unsafe { openlog(IDENT.as_ptr(), LOG_PID, LOG_USER) };
111 });
112 }
113
114 pub(super) unsafe fn emit(priority: c_int, msg: *const c_char) {
120 ensure_open();
121 unsafe { syslog(priority, c"%s".as_ptr(), msg) };
125 }
126}
127
128impl PlatformSink {
129 #[must_use]
138 pub fn from_config(config: &crate::config::Config) -> Self {
139 for dest in &config.logging_destinations {
140 match dest {
141 crate::config::LoggingDestination::File(path) => {
142 if let Ok(file) = std::fs::OpenOptions::new()
143 .create(true)
144 .append(true)
145 .open(path)
146 {
147 return Self::File(file);
148 }
149 }
150 crate::config::LoggingDestination::Stdout => {
151 return Self::Stdout;
152 }
153 crate::config::LoggingDestination::Network(_) => {
154 }
156 }
157 }
158 Self::native()
159 }
160
161 #[must_use]
163 #[allow(clippy::missing_const_for_fn)]
164 pub fn native() -> Self {
165 if std::env::var("RLG_FALLBACK_STDOUT").is_ok()
167 || std::env::var("GITHUB_ACTIONS").is_ok()
168 {
169 return Self::Stdout;
170 }
171
172 #[cfg(target_os = "macos")]
173 {
174 Self::OsLog
175 }
176 #[cfg(target_os = "linux")]
177 {
178 Self::detect_journald()
179 }
180 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
181 {
182 Self::Stdout
183 }
184 }
185
186 #[cfg(target_os = "linux")]
188 fn detect_journald() -> Self {
189 Self::try_journald_socket("/run/systemd/journal/socket")
190 }
191
192 #[cfg(target_os = "linux")]
194 fn try_journald_socket(path: &str) -> Self {
195 UnixDatagram::unbound()
196 .ok()
197 .and_then(|socket| {
198 socket.connect(path).ok().map(|()| socket)
199 })
200 .map_or(Self::Journald(None), |s| Self::Journald(Some(s)))
201 }
202
203 #[allow(unused_variables)]
205 pub fn emit(&mut self, level: &str, payload: &[u8]) {
206 match self {
207 Self::Stdout => {
208 let _ = std::io::stdout().write_all(payload);
209 let _ = std::io::stdout().write_all(b"\n");
210 }
211 Self::File(f) => {
212 let _ = f.write_all(payload);
213 let _ = f.write_all(b"\n");
214 }
215 Self::OsLog => Self::emit_os_log(level, payload),
216 Self::Journald(socket_opt) => {
217 Self::emit_journald(
218 level,
219 payload,
220 socket_opt.as_ref(),
221 );
222 }
223 #[cfg(all(target_os = "linux", feature = "uring"))]
224 Self::UringFile(f) => {
225 let _ = f.write_all(payload);
234 let _ = f.write_all(b"\n");
235 }
236 }
237 }
238
239 #[cfg(target_os = "macos")]
244 fn emit_os_log(level: &str, payload: &[u8]) {
245 use syslog_ffi::{
246 LOG_CRIT, LOG_DEBUG, LOG_ERR, LOG_INFO, LOG_NOTICE,
247 LOG_WARNING, emit,
248 };
249
250 if std::env::var("RLG_FALLBACK_STDOUT").is_ok()
251 || std::env::var("GITHUB_ACTIONS").is_ok()
252 {
253 let _ = std::io::stdout().write_all(payload);
254 let _ = std::io::stdout().write_all(b"\n");
255 return;
256 }
257
258 let priority = match level {
259 "FATAL" | "CRITICAL" => LOG_CRIT,
260 "ERROR" => LOG_ERR,
261 "WARN" => LOG_WARNING,
262 "INFO" => LOG_INFO,
263 "DEBUG" | "TRACE" | "VERBOSE" => LOG_DEBUG,
264 _ => LOG_NOTICE,
265 };
266 let mut buf: Vec<u8> =
269 payload.iter().copied().filter(|&b| b != 0).collect();
270 buf.push(0);
271 #[allow(unsafe_code)]
274 unsafe {
275 emit(priority, buf.as_ptr().cast::<std::os::raw::c_char>());
276 }
277 }
278
279 #[cfg(not(target_os = "macos"))]
281 const fn emit_os_log(_level: &str, _payload: &[u8]) {}
282
283 fn emit_journald(
289 level: &str,
290 payload: &[u8],
291 socket_opt: Option<&UnixDatagram>,
292 ) {
293 let Some(socket) = socket_opt else {
294 let _ = std::io::stdout().write_all(payload);
295 let _ = std::io::stdout().write_all(b"\n");
296 return;
297 };
298
299 if std::env::var("RLG_FALLBACK_STDOUT").is_ok()
300 || std::env::var("GITHUB_ACTIONS").is_ok()
301 {
302 let _ = socket;
303 return;
304 }
305
306 #[cfg(target_os = "linux")]
307 {
308 let priority = match level {
309 "ERROR" | "FATAL" | "CRITICAL" => "3",
310 "WARN" => "4",
311 "INFO" => "6",
312 "DEBUG" | "TRACE" | "VERBOSE" => "7",
313 _ => "5",
314 };
315 let mut journal_payload =
316 Vec::with_capacity(payload.len() + 32);
317 journal_payload.extend_from_slice(b"PRIORITY=");
318 journal_payload.extend_from_slice(priority.as_bytes());
319 journal_payload.extend_from_slice(b"\nMESSAGE=");
320 journal_payload.extend_from_slice(payload);
321 journal_payload.extend_from_slice(b"\n");
322 let _ = socket.send(&journal_payload);
323 }
324 #[cfg(not(target_os = "linux"))]
325 {
326 let _ = (level, payload, socket);
327 }
328 }
329}
330
331#[cfg(all(test, not(miri)))]
332mod tests {
333 use super::*;
334 use serial_test::serial;
335
336 #[test]
337 #[cfg_attr(miri, ignore)]
338 fn test_platform_sink_stdout() {
339 let mut sink = PlatformSink::Stdout;
340 sink.emit("INFO", b"test stdout");
341 }
342
343 #[test]
347 #[cfg_attr(miri, ignore)]
348 #[cfg(all(target_os = "linux", feature = "uring"))]
349 fn test_platform_sink_uring_file_scaffold_emits() {
350 let tmp = tempfile::NamedTempFile::new().unwrap();
351 let file = tmp.reopen().unwrap();
352 let mut sink = PlatformSink::UringFile(file);
353 sink.emit("INFO", b"uring scaffold test");
354 let contents = std::fs::read_to_string(tmp.path()).unwrap();
356 assert!(contents.contains("uring scaffold test"));
357 }
358
359 #[test]
360 #[cfg_attr(miri, ignore)]
361 #[allow(unsafe_code)]
362 #[serial]
363 fn test_platform_sink_fallback_env_var() {
364 unsafe { std::env::set_var("RLG_FALLBACK_STDOUT", "1") };
366 let sink = PlatformSink::native();
367 assert!(matches!(sink, PlatformSink::Stdout));
368 unsafe { std::env::remove_var("RLG_FALLBACK_STDOUT") };
370 }
371
372 #[test]
373 #[cfg_attr(miri, ignore)]
374 #[allow(unsafe_code)]
375 #[serial]
376 fn test_platform_sink_native_journald_path() {
377 unsafe {
379 std::env::remove_var("RLG_FALLBACK_STDOUT");
380 std::env::remove_var("GITHUB_ACTIONS");
381 }
382 let sink = PlatformSink::native();
383 #[cfg(target_os = "linux")]
384 assert!(matches!(sink, PlatformSink::Journald(_)));
385 #[cfg(target_os = "macos")]
386 assert!(matches!(sink, PlatformSink::OsLog));
387 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
388 assert!(matches!(sink, PlatformSink::Stdout));
389 unsafe { std::env::set_var("RLG_FALLBACK_STDOUT", "1") };
391 }
392
393 #[test]
394 #[cfg_attr(miri, ignore)]
395 #[cfg(target_os = "linux")]
396 fn test_try_journald_socket_failure() {
397 let sink =
398 PlatformSink::try_journald_socket("/nonexistent/path");
399 assert!(matches!(sink, PlatformSink::Journald(None)));
400 }
401
402 #[test]
403 #[cfg_attr(miri, ignore)]
404 fn test_platform_sink_journald_coverage() {
405 #[cfg(unix)]
406 {
407 let (sock1, _sock2) = UnixDatagram::pair().unwrap();
408 let mut sink = PlatformSink::Journald(Some(sock1));
409 sink.emit("INFO", b"test journald");
410 }
411
412 let mut sink_none = PlatformSink::Journald(None);
413 sink_none.emit("INFO", b"test journald fallback");
414 }
415
416 #[test]
417 #[cfg_attr(miri, ignore)]
418 #[allow(unsafe_code)]
419 #[serial]
420 fn test_platform_sink_oslog_fallback_stdout() {
421 unsafe { std::env::set_var("RLG_FALLBACK_STDOUT", "1") };
424 let mut sink = PlatformSink::OsLog;
425 sink.emit("INFO", b"fallback-test");
426 unsafe { std::env::remove_var("RLG_FALLBACK_STDOUT") };
428 }
429
430 #[cfg(target_os = "macos")]
431 #[test]
432 #[cfg_attr(miri, ignore)]
433 #[allow(unsafe_code)]
434 #[serial]
435 fn test_platform_sink_oslog_real_syslog_call() {
436 unsafe {
440 std::env::remove_var("RLG_FALLBACK_STDOUT");
441 std::env::remove_var("GITHUB_ACTIONS");
442 }
443 let mut sink = PlatformSink::OsLog;
444 for level in [
447 "FATAL",
448 "CRITICAL",
449 "ERROR",
450 "WARN",
451 "INFO",
452 "DEBUG",
453 "TRACE",
454 "VERBOSE",
455 "UNKNOWN_LEVEL",
456 ] {
457 sink.emit(level, b"rlg coverage test record");
458 }
459 unsafe { std::env::set_var("RLG_FALLBACK_STDOUT", "1") };
461 }
462
463 #[test]
464 #[cfg_attr(miri, ignore)]
465 fn test_platform_sink_oslog_with_embedded_nulls() {
466 let mut sink = PlatformSink::OsLog;
470 sink.emit("INFO", b"with\0embedded\0nulls");
471 }
472}