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}
43
44#[cfg(any(target_os = "macos", test))]
45#[allow(unsafe_code)]
46mod macos_ffi {
47 use std::os::raw::{c_char, c_void};
48 #[allow(dead_code)]
49 pub(super) type os_log_t = *mut c_void;
50 #[repr(transparent)]
51 #[allow(dead_code)]
52 pub(super) struct os_log_type_t(pub(super) u8);
53
54 #[allow(dead_code)]
55 pub(super) const OS_LOG_TYPE_DEFAULT: os_log_type_t =
56 os_log_type_t(0x00);
57 #[allow(dead_code)]
58 pub(super) const OS_LOG_TYPE_INFO: os_log_type_t =
59 os_log_type_t(0x01);
60 #[allow(dead_code)]
61 pub(super) const OS_LOG_TYPE_DEBUG: os_log_type_t =
62 os_log_type_t(0x02);
63 #[allow(dead_code)]
64 pub(super) const OS_LOG_TYPE_ERROR: os_log_type_t =
65 os_log_type_t(0x10);
66 #[allow(dead_code)]
67 pub(super) const OS_LOG_TYPE_FAULT: os_log_type_t =
68 os_log_type_t(0x11);
69
70 unsafe extern "C" {
71 #[allow(dead_code)]
72 pub(super) fn os_log_create(
73 subsystem: *const c_char,
74 category: *const c_char,
75 ) -> os_log_t;
76 #[allow(dead_code)]
77 pub(super) fn _os_log_impl(
78 dso: *mut c_void,
79 log: os_log_t,
80 log_type: os_log_type_t,
81 format: *const c_char,
82 buf: *const u8,
83 size: u32,
84 );
85 }
86}
87
88impl PlatformSink {
89 #[must_use]
98 pub fn from_config(config: &crate::config::Config) -> Self {
99 for dest in &config.logging_destinations {
100 match dest {
101 crate::config::LoggingDestination::File(path) => {
102 if let Ok(file) = std::fs::OpenOptions::new()
103 .create(true)
104 .append(true)
105 .open(path)
106 {
107 return Self::File(file);
108 }
109 }
110 crate::config::LoggingDestination::Stdout => {
111 return Self::Stdout;
112 }
113 crate::config::LoggingDestination::Network(_) => {
114 }
116 }
117 }
118 Self::native()
119 }
120
121 #[must_use]
123 #[allow(clippy::missing_const_for_fn)]
124 pub fn native() -> Self {
125 if std::env::var("RLG_FALLBACK_STDOUT").is_ok()
127 || std::env::var("GITHUB_ACTIONS").is_ok()
128 {
129 return Self::Stdout;
130 }
131
132 #[cfg(target_os = "macos")]
133 {
134 Self::OsLog
135 }
136 #[cfg(target_os = "linux")]
137 {
138 Self::detect_journald()
139 }
140 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
141 {
142 Self::Stdout
143 }
144 }
145
146 #[cfg(target_os = "linux")]
148 fn detect_journald() -> Self {
149 Self::try_journald_socket("/run/systemd/journal/socket")
150 }
151
152 #[cfg(target_os = "linux")]
154 fn try_journald_socket(path: &str) -> Self {
155 UnixDatagram::unbound()
156 .ok()
157 .and_then(|socket| {
158 socket.connect(path).ok().map(|()| socket)
159 })
160 .map_or(Self::Journald(None), |s| Self::Journald(Some(s)))
161 }
162
163 #[allow(unused_variables)]
165 #[allow(clippy::too_many_lines)]
166 pub fn emit(&mut self, level: &str, payload: &[u8]) {
167 match self {
168 Self::Stdout => {
169 let _ = std::io::stdout().write_all(payload);
170 let _ = std::io::stdout().write_all(b"\n");
171 }
172 Self::File(f) => {
173 let _ = f.write_all(payload);
174 let _ = f.write_all(b"\n");
175 }
176 Self::OsLog => {
177 #[cfg(target_os = "macos")]
178 {
179 if std::env::var("RLG_FALLBACK_STDOUT").is_ok()
180 || std::env::var("GITHUB_ACTIONS").is_ok()
181 {
182 let _ = (level, payload);
183 } else {
184 #[cfg(not(any(test, miri)))]
185 {
186 use macos_ffi::*;
187 use std::ffi::CString;
188
189 let subsystem =
190 CString::new("com.rlg.logger").unwrap();
191 let category =
192 CString::new("default").unwrap();
193
194 #[allow(unsafe_code)]
198 unsafe {
199 let log_handle = os_log_create(
200 subsystem.as_ptr(),
201 category.as_ptr(),
202 );
203 if log_handle.is_null() {
204 let _ = std::io::stdout()
206 .write_all(payload);
207 let _ = std::io::stdout()
208 .write_all(b"\n");
209 return;
210 }
211 let log_type = match level {
212 "ERROR" | "FATAL" => {
213 OS_LOG_TYPE_ERROR
214 }
215 "CRITICAL" => OS_LOG_TYPE_FAULT,
216 "WARN" => OS_LOG_TYPE_DEFAULT,
217 "INFO" => OS_LOG_TYPE_INFO,
218 "DEBUG" | "TRACE" | "VERBOSE" => {
219 OS_LOG_TYPE_DEBUG
220 }
221 _ => OS_LOG_TYPE_DEFAULT,
222 };
223
224 let format =
225 CString::new("%{public}s").unwrap();
226 let clean_payload: Vec<u8> = payload
228 .iter()
229 .copied()
230 .filter(|&b| b != 0)
231 .collect();
232 let msg = CString::new(clean_payload)
233 .unwrap_or_default();
234
235 _os_log_impl(
236 std::ptr::null_mut(),
237 log_handle,
238 log_type,
239 format.as_ptr(),
240 msg.as_ptr().cast::<u8>(),
241 msg.as_bytes().len() as u32,
242 );
243 }
244 }
245 #[cfg(any(test, miri))]
246 {
247 let _ = (level, payload);
248 }
249 }
250 }
251 #[cfg(not(target_os = "macos"))]
252 {
253 let _ = (level, payload);
254 }
255 }
256 Self::Journald(socket_opt) => {
257 if let Some(socket) = socket_opt {
258 #[cfg(any(test, miri))]
259 let _ = socket;
260 let priority = match level {
261 "ERROR" | "FATAL" | "CRITICAL" => "3",
262 "WARN" => "4",
263 "INFO" => "6",
264 "DEBUG" | "TRACE" | "VERBOSE" => "7",
265 _ => "5",
266 };
267
268 let mut journal_payload =
270 Vec::with_capacity(payload.len() + 32);
271 journal_payload.extend_from_slice(b"PRIORITY=");
272 journal_payload
273 .extend_from_slice(priority.as_bytes());
274 journal_payload.extend_from_slice(b"\nMESSAGE=");
275 journal_payload.extend_from_slice(payload);
276 journal_payload.extend_from_slice(b"\n");
277
278 if std::env::var("RLG_FALLBACK_STDOUT").is_ok()
279 || std::env::var("GITHUB_ACTIONS").is_ok()
280 {
281 let _ = journal_payload;
282 } else {
283 #[cfg(all(
284 target_os = "linux",
285 not(any(test, miri))
286 ))]
287 let _ = socket.send(&journal_payload);
288 #[cfg(any(
289 not(target_os = "linux"),
290 test,
291 miri
292 ))]
293 {
294 let _ = journal_payload;
295 }
296 }
297 } else {
298 let _ = std::io::stdout().write_all(payload);
299 let _ = std::io::stdout().write_all(b"\n");
300 }
301 }
302 }
303 }
304}
305
306#[cfg(all(test, not(miri)))]
307mod tests {
308 use super::*;
309 use serial_test::serial;
310
311 #[test]
312 #[cfg_attr(miri, ignore)]
313 fn test_platform_sink_stdout() {
314 let mut sink = PlatformSink::Stdout;
315 sink.emit("INFO", b"test stdout");
316 }
317
318 #[test]
319 #[cfg_attr(miri, ignore)]
320 #[allow(unsafe_code)]
321 #[serial]
322 fn test_platform_sink_fallback_env_var() {
323 unsafe { std::env::set_var("RLG_FALLBACK_STDOUT", "1") };
325 let sink = PlatformSink::native();
326 assert!(matches!(sink, PlatformSink::Stdout));
327 unsafe { std::env::remove_var("RLG_FALLBACK_STDOUT") };
329 }
330
331 #[test]
332 #[cfg_attr(miri, ignore)]
333 #[allow(unsafe_code)]
334 #[serial]
335 fn test_platform_sink_native_journald_path() {
336 unsafe {
338 std::env::remove_var("RLG_FALLBACK_STDOUT");
339 std::env::remove_var("GITHUB_ACTIONS");
340 }
341 let sink = PlatformSink::native();
342 #[cfg(target_os = "linux")]
343 assert!(matches!(sink, PlatformSink::Journald(_)));
344 #[cfg(target_os = "macos")]
345 assert!(matches!(sink, PlatformSink::OsLog));
346 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
347 assert!(matches!(sink, PlatformSink::Stdout));
348 unsafe { std::env::set_var("RLG_FALLBACK_STDOUT", "1") };
350 }
351
352 #[test]
353 #[cfg_attr(miri, ignore)]
354 #[cfg(target_os = "linux")]
355 fn test_try_journald_socket_failure() {
356 let sink =
357 PlatformSink::try_journald_socket("/nonexistent/path");
358 assert!(matches!(sink, PlatformSink::Journald(None)));
359 }
360
361 #[test]
362 #[cfg_attr(miri, ignore)]
363 fn test_platform_sink_journald_coverage() {
364 #[cfg(unix)]
365 {
366 let (sock1, _sock2) = UnixDatagram::pair().unwrap();
367 let mut sink = PlatformSink::Journald(Some(sock1));
368 sink.emit("INFO", b"test journald");
369 }
370
371 let mut sink_none = PlatformSink::Journald(None);
372 sink_none.emit("INFO", b"test journald fallback");
373 }
374}