ombrac_client/
logging.rs

1use std::ffi::c_char;
2use std::io::Write;
3use std::sync::{Arc, OnceLock};
4
5use arc_swap::ArcSwap;
6use tracing_subscriber::layer::SubscriberExt;
7use tracing_subscriber::util::SubscriberInitExt;
8use tracing_subscriber::{EnvFilter, Registry};
9
10use crate::config::LoggingConfig;
11
12/// A type alias for the C-style callback function pointer.
13pub type LogCallback = extern "C" fn(message: *const c_char);
14
15// A global, thread-safe, and lock-free handle to the registered log callback.
16static LOG_CALLBACK: OnceLock<ArcSwap<Option<LogCallback>>> = OnceLock::new();
17
18/// Returns a reference to the global `ArcSwap<Option<LogCallback>>`.
19/// Initializes it on first access.
20fn get_log_callback_handle() -> &'static ArcSwap<Option<LogCallback>> {
21    LOG_CALLBACK.get_or_init(|| ArcSwap::from(Arc::new(None)))
22}
23
24/// A custom writer that forwards formatted log messages to the FFI callback.
25struct FfiWriter;
26
27impl Write for FfiWriter {
28    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
29        let callback_handle = get_log_callback_handle();
30        let callback_arc = callback_handle.load();
31        if let Some(callback) = **callback_arc {
32            // Trim trailing newline, as the callback consumer might add its own.
33            let message_bytes = if buf.last() == Some(&b'\n') {
34                &buf[..buf.len() - 1]
35            } else {
36                buf
37            };
38
39            if let Ok(message_cstr) = std::ffi::CString::new(message_bytes) {
40                callback(message_cstr.as_ptr());
41            }
42        }
43        Ok(buf.len())
44    }
45
46    fn flush(&mut self) -> std::io::Result<()> {
47        Ok(())
48    }
49}
50
51/// Initializes logging for a standalone binary application.
52///
53/// This setup directs logs to `stdout`.
54pub fn init_for_binary(config: &LoggingConfig) {
55    let filter = EnvFilter::try_from_default_env()
56        .unwrap_or_else(|_| EnvFilter::new(config.log_level.as_deref().unwrap_or("info")));
57
58    let (non_blocking_writer, _guard) = tracing_appender::non_blocking(std::io::stdout());
59    std::mem::forget(_guard); // Keep the guard alive for the duration of the program.
60
61    let layer = tracing_subscriber::fmt::layer()
62        .with_writer(non_blocking_writer)
63        .with_thread_ids(true);
64
65    Registry::default().with(filter).with(layer).init();
66}
67
68/// Initializes logging for FFI consumers.
69///
70/// This setup directs logs to the registered FFI callback.
71pub fn init_for_ffi(config: &LoggingConfig) {
72    let filter = EnvFilter::try_from_default_env()
73        .unwrap_or_else(|_| EnvFilter::new(config.log_level.as_deref().unwrap_or("info")));
74
75    let (ffi_writer, _guard) = tracing_appender::non_blocking(FfiWriter);
76    std::mem::forget(_guard); // Keep the guard alive.
77
78    let layer = tracing_subscriber::fmt::layer()
79        .with_writer(ffi_writer)
80        .with_ansi(false) // ANSI codes are not expected by FFI consumers.
81        .with_target(true)
82        .with_thread_ids(true)
83        .with_level(true);
84
85    Registry::default().with(filter).with(layer).init();
86}
87
88/// Sets or clears the global log callback for FFI.
89///
90/// This function should be called before `init_for_ffi`.
91pub fn set_log_callback(callback: Option<LogCallback>) {
92    let callback_handle = get_log_callback_handle();
93    callback_handle.store(Arc::new(callback));
94}