libghostty_vt/log.rs
1//! Logging functionality.
2use std::sync::RwLock;
3
4use crate::{
5 error::{Error, Result},
6 ffi,
7};
8
9/// Callback type for logging.
10///
11/// When installed, internal library log messages are delivered through
12/// this callback instead of being discarded. The embedder is responsible
13/// for formatting and routing log output.
14///
15/// When the log is unscoped (default scope), scope has zero length.
16///
17/// The callback must be safe to call from any thread.
18///
19/// See [`set_logger`] for more details.
20pub trait Logger: Send + Sync + 'static {
21 /// Log a message with the given level and scope.
22 fn log(&self, level: Level, scope: &str, message: &str);
23}
24
25/// Built-in log callback that writes to stderr.
26///
27/// Formats each message as `[level](scope): message\n`.
28///
29/// Can be passed directly to [`set_logger`]:
30///
31/// ```
32/// use libghostty_vt::log;
33/// log::set_logger(Some(Box::new(log::LogStderr)));
34/// ```
35#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
36pub struct LogStderr;
37impl Logger for LogStderr {
38 fn log(&self, level: Level, scope: &str, message: &str) {
39 unsafe {
40 ffi::ghostty_sys_log_stderr(
41 std::ptr::null_mut(),
42 level.into(),
43 scope.as_ptr(),
44 scope.len(),
45 message.as_ptr(),
46 message.len(),
47 );
48 }
49 }
50}
51
52#[cfg(feature = "tracing")]
53const TRACING_TARGET: &str = "libghostty_vt::ghostty";
54
55/// Adapt `libghostty` logs into `tracing` events.
56///
57/// This is the preferred integration for applications that already use
58/// `tracing`. The upstream callback only provides a level, scope, and message,
59/// so the scope is recorded as the structured `ghostty_scope` field rather than
60/// as the `tracing` target.
61#[cfg(feature = "tracing")]
62#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
63#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
64pub struct TracingLogger;
65#[cfg(feature = "tracing")]
66impl Logger for TracingLogger {
67 fn log(&self, level: Level, scope: &str, message: &str) {
68 match level {
69 Level::Error => {
70 tracing::error!(target: TRACING_TARGET, ghostty_scope = scope, "{message}");
71 }
72 Level::Warning => {
73 tracing::warn!(target: TRACING_TARGET, ghostty_scope = scope, "{message}");
74 }
75 Level::Info => {
76 tracing::info!(target: TRACING_TARGET, ghostty_scope = scope, "{message}");
77 }
78 Level::Debug => {
79 tracing::debug!(target: TRACING_TARGET, ghostty_scope = scope, "{message}");
80 }
81 }
82 }
83}
84
85/// Adapt a `log` implementation to be used by `libghostty`.
86///
87/// `libghostty` log scopes are translated directly to `log`'s metadata
88/// target, and `log` implementation can choose to filter specific
89/// `libghostty` logs to be emitted.
90#[cfg(feature = "log")]
91impl<L: log::Log + 'static> Logger for L {
92 fn log(&self, level: Level, scope: &str, message: &str) {
93 let level = match level {
94 Level::Error => log::Level::Error,
95 Level::Warning => log::Level::Warn,
96 Level::Info => log::Level::Info,
97 Level::Debug => log::Level::Debug,
98 };
99 let args = format_args!("{message}");
100 let record = log::Record::builder()
101 .level(level)
102 .target(scope)
103 .args(args)
104 .build();
105
106 log::Log::log(&self, &record);
107 }
108}
109
110/// Log severity levels for the log callback.
111#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, int_enum::IntEnum)]
112#[repr(u32)]
113#[non_exhaustive]
114#[expect(missing_docs, reason = "missing upstream docs")]
115pub enum Level {
116 Error = ffi::SysLogLevel::ERROR,
117 Warning = ffi::SysLogLevel::WARNING,
118 Info = ffi::SysLogLevel::INFO,
119 Debug = ffi::SysLogLevel::DEBUG,
120}
121
122static LOGGER: RwLock<Option<Box<dyn Logger>>> = RwLock::new(None);
123
124/// Set the log callback.
125///
126/// When set, internal library log messages are delivered to this
127/// callback. When cleared (set to `None`), log messages are silently
128/// discarded.
129///
130/// Which log levels are emitted depends on the build mode of the library and
131/// is not configurable at runtime. Debug builds emit all levels (debug and
132/// above). Release builds emit info and above; debug-level messages are
133/// compiled out entirely and will never reach the callback.
134///
135/// # Examples
136///
137/// Use [`LogStderr`] as a simple way to write formatted logs to stderr:
138///
139/// ```
140/// use libghostty_vt::{set_logger, log::LogStderr};
141/// # fn main() -> Result<(), Box<dyn std::error::Error>>{
142/// set_logger(Some(Box::new(LogStderr)))?;
143/// # Ok(())}
144/// ```
145///
146/// When the `tracing` feature is enabled, you can redirect log messages to
147/// `tracing` events with `TracingLogger`:
148///
149/// ```ignore
150/// # fn main() -> Result<(), Box<dyn std::error::Error>>{
151/// tracing_subscriber::fmt().init();
152/// libghostty_vt::set_logger(Some(Box::new(libghostty_vt::log::TracingLogger)))?;
153/// # Ok(())}
154/// ```
155///
156/// When the `log` feature is enabled, you can also redirect log messages
157/// to any `log`-compatible logger, including the one currently used globally:
158///
159/// ```ignore
160/// # fn main() -> Result<(), Box<dyn std::error::Error>>{
161/// // Any logger will do, though usually you want to use the global logger
162/// libghostty_vt::set_logger(Some(Box::new(log::logger())))?;
163/// # Ok(())}
164/// ```
165pub fn set_logger(f: Option<Box<dyn Logger>>) -> Result<()> {
166 unsafe extern "C" fn callback(
167 _userdata: *mut std::ffi::c_void,
168 level: ffi::SysLogLevel::Type,
169 scope: *const u8,
170 scope_len: usize,
171 message: *const u8,
172 message_len: usize,
173 ) {
174 let scope = unsafe { std::slice::from_raw_parts(scope, scope_len) };
175 let Ok(scope) = std::str::from_utf8(scope) else {
176 return;
177 };
178 let message = unsafe { std::slice::from_raw_parts(message, message_len) };
179 let Ok(message) = std::str::from_utf8(message) else {
180 return;
181 };
182 let Ok(level) = Level::try_from(level) else {
183 return;
184 };
185
186 let Ok(log) = LOGGER.read() else {
187 return;
188 };
189 let Some(log) = log.as_deref() else {
190 return;
191 };
192 log.log(level, scope, message);
193 }
194
195 // Write out the matches here to coerce function items into function
196 // pointers, and trait impls into boxed trait objects. Yes, this is
197 // the simplest way to do so.
198 let ptr: ffi::SysLogFn = match f {
199 None => None,
200 Some(_) => Some(callback),
201 };
202 {
203 let Ok(mut logger) = LOGGER.write() else {
204 return Err(Error::InvalidValue);
205 };
206 *logger = f;
207 }
208 crate::sys_set(
209 ffi::SysOption::GHOSTTY_SYS_OPT_LOG,
210 ptr.map_or(std::ptr::null(), |p| p as *const std::ffi::c_void),
211 )
212}