1use std::io::Write;
13
14#[cfg(unix)]
15use std::os::unix::net::UnixDatagram;
16
17#[cfg(not(unix))]
23#[allow(dead_code)]
24#[derive(Debug, Clone, Copy)]
25pub struct UnixDatagram;
26
27#[cfg(not(unix))]
28#[allow(dead_code)]
29impl UnixDatagram {
30 pub const fn send(
36 &self,
37 _payload: &[u8],
38 ) -> std::io::Result<usize> {
39 Ok(0)
40 }
41}
42
43#[derive(Debug)]
45#[allow(variant_size_differences)]
46pub enum PlatformSink {
47 Stdout,
49 File(std::fs::File),
51 OsLog,
53 Journald(Option<UnixDatagram>),
55 #[cfg(all(target_os = "linux", feature = "uring"))]
67 #[cfg_attr(
68 docsrs,
69 doc(cfg(all(target_os = "linux", feature = "uring")))
70 )]
71 UringFile(std::fs::File),
72}
73
74#[cfg(target_os = "macos")]
84#[allow(unsafe_code)]
85mod syslog_ffi {
86 use std::ffi::CStr;
87 use std::os::raw::{c_char, c_int};
88 use std::sync::OnceLock;
89
90 pub(super) const LOG_CRIT: c_int = 2;
91 pub(super) const LOG_ERR: c_int = 3;
92 pub(super) const LOG_WARNING: c_int = 4;
93 pub(super) const LOG_NOTICE: c_int = 5;
94 pub(super) const LOG_INFO: c_int = 6;
95 pub(super) const LOG_DEBUG: c_int = 7;
96 const LOG_USER: c_int = 1 << 3;
97 const LOG_PID: c_int = 0x01;
98
99 unsafe extern "C" {
100 fn openlog(
101 ident: *const c_char,
102 logopt: c_int,
103 facility: c_int,
104 );
105 fn syslog(priority: c_int, format: *const c_char, ...);
110 }
111
112 static INIT: OnceLock<()> = OnceLock::new();
113
114 fn ensure_open() {
115 INIT.get_or_init(|| {
116 const IDENT: &CStr = c"rlg";
121 unsafe { openlog(IDENT.as_ptr(), LOG_PID, LOG_USER) };
124 });
125 }
126
127 pub(super) unsafe fn emit(priority: c_int, msg: *const c_char) {
133 ensure_open();
134 unsafe { syslog(priority, c"%s".as_ptr(), msg) };
138 }
139}
140
141impl PlatformSink {
142 #[must_use]
151 pub fn from_config(config: &crate::config::Config) -> Self {
152 for dest in &config.logging_destinations {
153 match dest {
154 crate::config::LoggingDestination::File(path) => {
155 if let Ok(file) = std::fs::OpenOptions::new()
156 .create(true)
157 .append(true)
158 .open(path)
159 {
160 return Self::File(file);
161 }
162 }
163 crate::config::LoggingDestination::Stdout => {
164 return Self::Stdout;
165 }
166 crate::config::LoggingDestination::Network(_) => {
167 }
169 }
170 }
171 Self::native()
172 }
173
174 #[must_use]
176 #[allow(clippy::missing_const_for_fn)]
177 pub fn native() -> Self {
178 if std::env::var("RLG_FALLBACK_STDOUT").is_ok()
180 || std::env::var("GITHUB_ACTIONS").is_ok()
181 {
182 return Self::Stdout;
183 }
184
185 #[cfg(target_os = "macos")]
186 {
187 Self::OsLog
188 }
189 #[cfg(target_os = "linux")]
190 {
191 Self::detect_journald()
192 }
193 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
194 {
195 Self::Stdout
196 }
197 }
198
199 #[cfg(target_os = "linux")]
201 fn detect_journald() -> Self {
202 Self::try_journald_socket("/run/systemd/journal/socket")
203 }
204
205 #[cfg(target_os = "linux")]
207 fn try_journald_socket(path: &str) -> Self {
208 UnixDatagram::unbound()
209 .ok()
210 .and_then(|socket| {
211 socket.connect(path).ok().map(|()| socket)
212 })
213 .map_or(Self::Journald(None), |s| Self::Journald(Some(s)))
214 }
215
216 #[allow(unused_variables)]
218 pub fn emit(&mut self, level: &str, payload: &[u8]) {
219 match self {
220 Self::Stdout => {
221 let _ = std::io::stdout().write_all(payload);
222 let _ = std::io::stdout().write_all(b"\n");
223 }
224 Self::File(f) => {
225 let _ = f.write_all(payload);
226 let _ = f.write_all(b"\n");
227 }
228 Self::OsLog => Self::emit_os_log(level, payload),
229 Self::Journald(socket_opt) => {
230 Self::emit_journald(
231 level,
232 payload,
233 socket_opt.as_ref(),
234 );
235 }
236 #[cfg(all(target_os = "linux", feature = "uring"))]
237 Self::UringFile(f) => {
238 let _ = f.write_all(payload);
247 let _ = f.write_all(b"\n");
248 }
249 }
250 }
251
252 #[cfg(target_os = "macos")]
257 fn emit_os_log(level: &str, payload: &[u8]) {
258 use syslog_ffi::{
259 LOG_CRIT, LOG_DEBUG, LOG_ERR, LOG_INFO, LOG_NOTICE,
260 LOG_WARNING, emit,
261 };
262
263 if std::env::var("RLG_FALLBACK_STDOUT").is_ok()
264 || std::env::var("GITHUB_ACTIONS").is_ok()
265 {
266 let _ = std::io::stdout().write_all(payload);
267 let _ = std::io::stdout().write_all(b"\n");
268 return;
269 }
270
271 let priority = match level {
272 "FATAL" | "CRITICAL" => LOG_CRIT,
273 "ERROR" => LOG_ERR,
274 "WARN" => LOG_WARNING,
275 "INFO" => LOG_INFO,
276 "DEBUG" | "TRACE" | "VERBOSE" => LOG_DEBUG,
277 _ => LOG_NOTICE,
278 };
279 let mut buf: Vec<u8> =
282 payload.iter().copied().filter(|&b| b != 0).collect();
283 buf.push(0);
284 #[allow(unsafe_code)]
287 unsafe {
288 emit(priority, buf.as_ptr().cast::<std::os::raw::c_char>());
289 }
290 }
291
292 #[cfg(not(target_os = "macos"))]
294 const fn emit_os_log(_level: &str, _payload: &[u8]) {}
295
296 fn emit_journald(
302 level: &str,
303 payload: &[u8],
304 socket_opt: Option<&UnixDatagram>,
305 ) {
306 let Some(socket) = socket_opt else {
307 let _ = std::io::stdout().write_all(payload);
308 let _ = std::io::stdout().write_all(b"\n");
309 return;
310 };
311
312 if std::env::var("RLG_FALLBACK_STDOUT").is_ok()
313 || std::env::var("GITHUB_ACTIONS").is_ok()
314 {
315 let _ = socket;
316 return;
317 }
318
319 #[cfg(target_os = "linux")]
320 {
321 let priority = match level {
322 "ERROR" | "FATAL" | "CRITICAL" => "3",
323 "WARN" => "4",
324 "INFO" => "6",
325 "DEBUG" | "TRACE" | "VERBOSE" => "7",
326 _ => "5",
327 };
328 let mut journal_payload =
329 Vec::with_capacity(payload.len() + 32);
330 journal_payload.extend_from_slice(b"PRIORITY=");
331 journal_payload.extend_from_slice(priority.as_bytes());
332 journal_payload.extend_from_slice(b"\nMESSAGE=");
333 journal_payload.extend_from_slice(payload);
334 journal_payload.extend_from_slice(b"\n");
335 let _ = socket.send(&journal_payload);
336 }
337 #[cfg(not(target_os = "linux"))]
338 {
339 let _ = (level, payload, socket);
340 }
341 }
342}
343
344#[cfg(all(test, not(miri)))]
345mod tests {
346 use super::*;
347 use serial_test::serial;
348
349 #[test]
350 #[cfg_attr(miri, ignore)]
351 fn test_platform_sink_stdout() {
352 let mut sink = PlatformSink::Stdout;
353 sink.emit("INFO", b"test stdout");
354 }
355
356 #[test]
360 #[cfg_attr(miri, ignore)]
361 #[cfg(all(target_os = "linux", feature = "uring"))]
362 fn test_platform_sink_uring_file_scaffold_emits() {
363 let tmp = tempfile::NamedTempFile::new().unwrap();
364 let file = tmp.reopen().unwrap();
365 let mut sink = PlatformSink::UringFile(file);
366 sink.emit("INFO", b"uring scaffold test");
367 let contents = std::fs::read_to_string(tmp.path()).unwrap();
369 assert!(contents.contains("uring scaffold test"));
370 }
371
372 #[test]
373 #[cfg_attr(miri, ignore)]
374 #[allow(unsafe_code)]
375 #[serial]
376 fn test_platform_sink_fallback_env_var() {
377 unsafe { std::env::set_var("RLG_FALLBACK_STDOUT", "1") };
379 let sink = PlatformSink::native();
380 assert!(matches!(sink, PlatformSink::Stdout));
381 unsafe { std::env::remove_var("RLG_FALLBACK_STDOUT") };
383 }
384
385 #[test]
386 #[cfg_attr(miri, ignore)]
387 #[allow(unsafe_code)]
388 #[serial]
389 fn test_platform_sink_native_journald_path() {
390 unsafe {
392 std::env::remove_var("RLG_FALLBACK_STDOUT");
393 std::env::remove_var("GITHUB_ACTIONS");
394 }
395 let sink = PlatformSink::native();
396 #[cfg(target_os = "linux")]
397 assert!(matches!(sink, PlatformSink::Journald(_)));
398 #[cfg(target_os = "macos")]
399 assert!(matches!(sink, PlatformSink::OsLog));
400 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
401 assert!(matches!(sink, PlatformSink::Stdout));
402 unsafe { std::env::set_var("RLG_FALLBACK_STDOUT", "1") };
404 }
405
406 #[test]
407 #[cfg_attr(miri, ignore)]
408 #[cfg(target_os = "linux")]
409 fn test_try_journald_socket_failure() {
410 let sink =
411 PlatformSink::try_journald_socket("/nonexistent/path");
412 assert!(matches!(sink, PlatformSink::Journald(None)));
413 }
414
415 #[test]
416 #[cfg_attr(miri, ignore)]
417 fn test_platform_sink_journald_coverage() {
418 #[cfg(unix)]
419 {
420 let (sock1, _sock2) = UnixDatagram::pair().unwrap();
421 let mut sink = PlatformSink::Journald(Some(sock1));
422 sink.emit("INFO", b"test journald");
423 }
424
425 let mut sink_none = PlatformSink::Journald(None);
426 sink_none.emit("INFO", b"test journald fallback");
427 }
428
429 #[test]
430 #[cfg_attr(miri, ignore)]
431 #[allow(unsafe_code)]
432 #[serial]
433 fn test_platform_sink_oslog_fallback_stdout() {
434 unsafe { std::env::set_var("RLG_FALLBACK_STDOUT", "1") };
437 let mut sink = PlatformSink::OsLog;
438 sink.emit("INFO", b"fallback-test");
439 unsafe { std::env::remove_var("RLG_FALLBACK_STDOUT") };
441 }
442
443 #[cfg(target_os = "macos")]
444 #[test]
445 #[cfg_attr(miri, ignore)]
446 #[allow(unsafe_code)]
447 #[serial]
448 fn test_platform_sink_oslog_real_syslog_call() {
449 unsafe {
453 std::env::remove_var("RLG_FALLBACK_STDOUT");
454 std::env::remove_var("GITHUB_ACTIONS");
455 }
456 let mut sink = PlatformSink::OsLog;
457 for level in [
460 "FATAL",
461 "CRITICAL",
462 "ERROR",
463 "WARN",
464 "INFO",
465 "DEBUG",
466 "TRACE",
467 "VERBOSE",
468 "UNKNOWN_LEVEL",
469 ] {
470 sink.emit(level, b"rlg coverage test record");
471 }
472 unsafe { std::env::set_var("RLG_FALLBACK_STDOUT", "1") };
474 }
475
476 #[test]
477 #[cfg_attr(miri, ignore)]
478 fn test_platform_sink_oslog_with_embedded_nulls() {
479 let mut sink = PlatformSink::OsLog;
483 sink.emit("INFO", b"with\0embedded\0nulls");
484 }
485}