Skip to main content

llama_cpp_bindings/
log.rs

1use crate::log_options::LogOptions;
2use std::sync::OnceLock;
3use tracing_core::{Interest, Kind, Metadata, callsite, field, identify_callsite};
4
5static FIELD_NAMES: &[&str] = &["message", "module"];
6
7struct OverridableFields {
8    message: tracing::field::Field,
9    target: tracing::field::Field,
10}
11
12macro_rules! log_cs {
13    ($level:expr, $cs:ident, $meta:ident, $fields:ident, $ty:ident) => {
14        struct $ty;
15        static $cs: $ty = $ty;
16        static $meta: Metadata<'static> = Metadata::new(
17            "log event",
18            "llama-cpp-bindings",
19            $level,
20            ::core::option::Option::None,
21            ::core::option::Option::None,
22            ::core::option::Option::None,
23            field::FieldSet::new(FIELD_NAMES, identify_callsite!(&$cs)),
24            Kind::EVENT,
25        );
26        static $fields: std::sync::LazyLock<OverridableFields> = std::sync::LazyLock::new(|| {
27            let fields = $meta.fields();
28            OverridableFields {
29                message: fields
30                    .field("message")
31                    .expect("message field defined in FIELD_NAMES"),
32                target: fields
33                    .field("module")
34                    .expect("module field defined in FIELD_NAMES"),
35            }
36        });
37
38        impl callsite::Callsite for $ty {
39            fn set_interest(&self, _: Interest) {}
40            fn metadata(&self) -> &'static Metadata<'static> {
41                &$meta
42            }
43        }
44    };
45}
46log_cs!(
47    tracing_core::Level::DEBUG,
48    DEBUG_CS,
49    DEBUG_META,
50    DEBUG_FIELDS,
51    DebugCallsite
52);
53log_cs!(
54    tracing_core::Level::INFO,
55    INFO_CS,
56    INFO_META,
57    INFO_FIELDS,
58    InfoCallsite
59);
60log_cs!(
61    tracing_core::Level::WARN,
62    WARN_CS,
63    WARN_META,
64    WARN_FIELDS,
65    WarnCallsite
66);
67log_cs!(
68    tracing_core::Level::ERROR,
69    ERROR_CS,
70    ERROR_META,
71    ERROR_FIELDS,
72    ErrorCallsite
73);
74
75#[derive(Clone, Copy)]
76pub enum Module {
77    Ggml,
78    LlamaCpp,
79}
80
81impl Module {
82    const fn name(self) -> &'static str {
83        match self {
84            Self::Ggml => "ggml",
85            Self::LlamaCpp => "llama.cpp",
86        }
87    }
88}
89
90fn meta_for_level(
91    level: llama_cpp_bindings_sys::ggml_log_level,
92) -> Option<(&'static Metadata<'static>, &'static OverridableFields)> {
93    match level {
94        llama_cpp_bindings_sys::GGML_LOG_LEVEL_DEBUG => Some((&DEBUG_META, &DEBUG_FIELDS)),
95        llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO => Some((&INFO_META, &INFO_FIELDS)),
96        llama_cpp_bindings_sys::GGML_LOG_LEVEL_WARN => Some((&WARN_META, &WARN_FIELDS)),
97        llama_cpp_bindings_sys::GGML_LOG_LEVEL_ERROR => Some((&ERROR_META, &ERROR_FIELDS)),
98        _ => None,
99    }
100}
101
102pub struct State {
103    pub options: LogOptions,
104    module: Module,
105    buffered: std::sync::Mutex<Option<(llama_cpp_bindings_sys::ggml_log_level, String)>>,
106    previous_level: std::sync::atomic::AtomicI32,
107    is_buffering: std::sync::atomic::AtomicBool,
108}
109
110impl State {
111    #[must_use]
112    pub fn new(module: Module, options: LogOptions) -> Self {
113        Self {
114            options,
115            module,
116            buffered: std::sync::Mutex::default(),
117            previous_level: std::sync::atomic::AtomicI32::default(),
118            is_buffering: std::sync::atomic::AtomicBool::default(),
119        }
120    }
121
122    fn generate_log(&self, level: llama_cpp_bindings_sys::ggml_log_level, text: &str) {
123        // Tracing requires the target name to be a string literal (not even &'static str), so
124        // the match arms below are duplicated per module. The interior submodule name from
125        // llama.cpp/ggml cannot be propagated as a target because it is baked into a static
126        // variable by the tracing macro at compile time.
127
128        let (module, text) = text
129            .char_indices()
130            .take_while(|(_, ch)| ch.is_ascii_lowercase() || *ch == '_')
131            .last()
132            .and_then(|(pos, _)| {
133                let next_two = text.get(pos + 1..pos + 3);
134                if next_two == Some(": ") {
135                    let (sub_module, text) = text.split_at(pos + 1);
136                    let text = text.split_at(2).1;
137                    Some((Some(format!("{}::{sub_module}", self.module.name())), text))
138                } else {
139                    None
140                }
141            })
142            .unwrap_or((None, text));
143
144        let effective_level = if self.options.demote_info_to_debug
145            && (level == llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO
146                || level == llama_cpp_bindings_sys::GGML_LOG_LEVEL_DEBUG)
147        {
148            llama_cpp_bindings_sys::GGML_LOG_LEVEL_DEBUG
149        } else {
150            level
151        };
152
153        let Some((meta, fields)) = meta_for_level(effective_level) else {
154            tracing::warn!(
155                level = effective_level,
156                text = text,
157                origin = "crate",
158                "generate_log called with unmapped log level"
159            );
160
161            return;
162        };
163
164        tracing::dispatcher::get_default(|dispatcher| {
165            dispatcher.event(&tracing::Event::new(
166                meta,
167                &meta.fields().value_set(&[
168                    (&fields.message, Some(&text as &dyn tracing::field::Value)),
169                    (
170                        &fields.target,
171                        module
172                            .as_ref()
173                            .map(|module_name| module_name as &dyn tracing::field::Value),
174                    ),
175                ]),
176            ));
177        });
178    }
179
180    /// Append more text to the previously buffered log.
181    ///
182    /// The text may or may not end with a newline.
183    ///
184    /// # Panics
185    /// Panics if the internal mutex is poisoned.
186    pub fn cont_buffered_log(&self, text: &str) {
187        let mut lock = self.buffered.lock().unwrap();
188
189        if let Some((previous_log_level, mut buffer)) = lock.take() {
190            buffer.push_str(text);
191            if buffer.ends_with('\n') {
192                self.is_buffering
193                    .store(false, std::sync::atomic::Ordering::Release);
194                self.generate_log(previous_log_level, buffer.as_str());
195            } else {
196                *lock = Some((previous_log_level, buffer));
197            }
198        } else {
199            let level = self
200                .previous_level
201                .load(std::sync::atomic::Ordering::Acquire)
202                .cast_unsigned();
203            tracing::warn!(
204                inferred_level = level,
205                text = text,
206                origin = "crate",
207                "llama.cpp sent out a CONT log without any previously buffered message"
208            );
209            *lock = Some((level, text.to_string()));
210        }
211    }
212
213    /// Start buffering a message. Not the CONT log level and text is missing a newline.
214    ///
215    /// # Panics
216    /// Panics if the internal mutex is poisoned.
217    pub fn buffer_non_cont(&self, level: llama_cpp_bindings_sys::ggml_log_level, text: &str) {
218        let replaced = self
219            .buffered
220            .lock()
221            .unwrap()
222            .replace((level, text.to_string()));
223
224        if let Some((previous_log_level, buffer)) = replaced {
225            tracing::warn!(
226                level = previous_log_level,
227                text = &buffer,
228                origin = "crate",
229                "Message buffered unnecessarily due to missing newline and not followed by a CONT"
230            );
231            self.generate_log(previous_log_level, buffer.as_str());
232        }
233
234        self.is_buffering
235            .store(true, std::sync::atomic::Ordering::Release);
236        self.previous_level
237            .store(level.cast_signed(), std::sync::atomic::Ordering::Release);
238    }
239
240    /// Emit a normal unbuffered log message (not the CONT log level and the text ends with a newline).
241    ///
242    /// # Panics
243    /// Panics if the internal mutex is poisoned.
244    pub fn emit_non_cont_line(&self, level: llama_cpp_bindings_sys::ggml_log_level, text: &str) {
245        if self
246            .is_buffering
247            .swap(false, std::sync::atomic::Ordering::Acquire)
248            && let Some((buf_level, buf_text)) = self.buffered.lock().unwrap().take()
249        {
250            // This warning indicates a bug within llama.cpp
251            tracing::warn!(
252                level = buf_level,
253                text = buf_text,
254                origin = "crate",
255                "llama.cpp message buffered spuriously due to missing \\n and being followed by a non-CONT message!"
256            );
257            self.generate_log(buf_level, buf_text.as_str());
258        }
259
260        self.previous_level
261            .store(level.cast_signed(), std::sync::atomic::Ordering::Release);
262
263        let (text, _trailing_newline) = text.split_at(text.len() - 1);
264
265        match level {
266            llama_cpp_bindings_sys::GGML_LOG_LEVEL_NONE => {
267                if self.options.demote_info_to_debug {
268                    self.generate_log(llama_cpp_bindings_sys::GGML_LOG_LEVEL_DEBUG, text);
269                } else {
270                    tracing::info!(no_log_level = true, text);
271                }
272            }
273            llama_cpp_bindings_sys::GGML_LOG_LEVEL_DEBUG
274            | llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO
275            | llama_cpp_bindings_sys::GGML_LOG_LEVEL_WARN
276            | llama_cpp_bindings_sys::GGML_LOG_LEVEL_ERROR => self.generate_log(level, text),
277            llama_cpp_bindings_sys::GGML_LOG_LEVEL_CONT => {
278                tracing::warn!(
279                    text = text,
280                    origin = "crate",
281                    "CONT log level passed to emit_non_cont_line"
282                );
283            }
284            _ => {
285                tracing::warn!(
286                    level = level,
287                    text = text,
288                    origin = "crate",
289                    "Unknown llama.cpp log level"
290                );
291            }
292        }
293    }
294
295    pub fn update_previous_level_for_disabled_log(
296        &self,
297        level: llama_cpp_bindings_sys::ggml_log_level,
298    ) {
299        if level != llama_cpp_bindings_sys::GGML_LOG_LEVEL_CONT {
300            self.previous_level
301                .store(level.cast_signed(), std::sync::atomic::Ordering::Release);
302        }
303    }
304
305    /// Checks whether the given log level is enabled by the current tracing subscriber.
306    pub fn is_enabled_for_level(&self, level: llama_cpp_bindings_sys::ggml_log_level) -> bool {
307        // CONT logs do not need to check if they are enabled.
308        let level = if level == llama_cpp_bindings_sys::GGML_LOG_LEVEL_CONT {
309            self.previous_level
310                .load(std::sync::atomic::Ordering::Relaxed)
311                .cast_unsigned()
312        } else {
313            level
314        };
315
316        let effective_level = if self.options.demote_info_to_debug
317            && (level == llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO
318                || level == llama_cpp_bindings_sys::GGML_LOG_LEVEL_DEBUG)
319        {
320            llama_cpp_bindings_sys::GGML_LOG_LEVEL_DEBUG
321        } else {
322            level
323        };
324
325        let Some((meta, _)) = meta_for_level(effective_level) else {
326            return false;
327        };
328
329        tracing::dispatcher::get_default(|dispatcher| dispatcher.enabled(meta))
330    }
331}
332
333pub static LLAMA_STATE: OnceLock<Box<State>> = OnceLock::new();
334pub static GGML_STATE: OnceLock<Box<State>> = OnceLock::new();
335
336extern "C" fn logs_to_trace(
337    level: llama_cpp_bindings_sys::ggml_log_level,
338    text: *const ::std::os::raw::c_char,
339    data: *mut ::std::os::raw::c_void,
340) {
341    // In the "fast-path" (i.e. the vast majority of logs) we want to avoid needing to take the log state
342    // lock at all. Similarly, we try to avoid any heap allocations within this function. This is accomplished
343    // by being a dummy pass-through to tracing in the normal case of DEBUG/INFO/WARN/ERROR logs that are
344    // newline terminated and limiting the slow-path of locks and/or heap allocations for other cases.
345    use std::borrow::Borrow;
346
347    let log_state = unsafe { &*(data as *const State) };
348
349    if log_state.options.disabled {
350        return;
351    }
352
353    // If the log level is disabled, we can just return early
354    if !log_state.is_enabled_for_level(level) {
355        log_state.update_previous_level_for_disabled_log(level);
356
357        return;
358    }
359
360    let text = unsafe { std::ffi::CStr::from_ptr(text) };
361    let text = text.to_string_lossy();
362    let text: &str = text.borrow();
363
364    // As best I can tell llama.cpp / ggml require all log format strings at call sites to have the '\n'.
365    // If it's missing, it means that you expect more logs via CONT (or there's a typo in the codebase). To
366    // distinguish typo from intentional support for CONT, we have to buffer until the next message comes in
367    // to know how to flush it.
368
369    if level == llama_cpp_bindings_sys::GGML_LOG_LEVEL_CONT {
370        log_state.cont_buffered_log(text);
371    } else if text.ends_with('\n') {
372        log_state.emit_non_cont_line(level, text);
373    } else {
374        log_state.buffer_non_cont(level, text);
375    }
376}
377
378/// Redirect llama.cpp logs into tracing.
379pub fn send_logs_to_tracing(options: LogOptions) {
380    // We set up separate log states for llama.cpp and ggml to make sure that CONT logs between the two
381    // can't possibly interfere with each other. In other words, if llama.cpp emits a log without a trailing
382    // newline and calls a GGML function, the logs won't be weirdly intermixed and instead we'll llama.cpp logs
383    // will CONT previous llama.cpp logs and GGML logs will CONT previous ggml logs.
384    let llama_heap_state = Box::as_ref(
385        LLAMA_STATE.get_or_init(|| Box::new(State::new(Module::LlamaCpp, options.clone()))),
386    ) as *const _;
387    let ggml_heap_state =
388        Box::as_ref(GGML_STATE.get_or_init(|| Box::new(State::new(Module::Ggml, options))))
389            as *const _;
390
391    unsafe {
392        // GGML has to be set after llama since setting llama sets ggml as well.
393        llama_cpp_bindings_sys::llama_log_set(Some(logs_to_trace), llama_heap_state as *mut _);
394        llama_cpp_bindings_sys::ggml_log_set(Some(logs_to_trace), ggml_heap_state as *mut _);
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use std::sync::{Arc, Mutex};
401
402    use tracing_subscriber::util::SubscriberInitExt;
403
404    use super::{Module, State, logs_to_trace};
405    use crate::log_options::LogOptions;
406
407    #[test]
408    fn module_name_ggml() {
409        assert_eq!(Module::Ggml.name(), "ggml");
410    }
411
412    #[test]
413    fn module_name_llama_cpp() {
414        assert_eq!(Module::LlamaCpp.name(), "llama.cpp");
415    }
416
417    #[test]
418    fn state_new_creates_empty_buffer() {
419        let state = State::new(Module::LlamaCpp, LogOptions::default());
420        let buffer = state
421            .buffered
422            .lock()
423            .unwrap_or_else(std::sync::PoisonError::into_inner);
424
425        assert!(buffer.is_none());
426        drop(buffer);
427        assert!(!state.options.disabled);
428    }
429
430    #[test]
431    fn update_previous_level_for_disabled_log_stores_level() {
432        let state = State::new(Module::LlamaCpp, LogOptions::default());
433
434        state.update_previous_level_for_disabled_log(llama_cpp_bindings_sys::GGML_LOG_LEVEL_WARN);
435
436        let stored = state
437            .previous_level
438            .load(std::sync::atomic::Ordering::Relaxed);
439
440        assert_eq!(
441            stored,
442            llama_cpp_bindings_sys::GGML_LOG_LEVEL_WARN.cast_signed()
443        );
444    }
445
446    #[test]
447    fn update_previous_level_ignores_cont() {
448        let state = State::new(Module::LlamaCpp, LogOptions::default());
449
450        state.update_previous_level_for_disabled_log(llama_cpp_bindings_sys::GGML_LOG_LEVEL_ERROR);
451        state.update_previous_level_for_disabled_log(llama_cpp_bindings_sys::GGML_LOG_LEVEL_CONT);
452
453        let stored = state
454            .previous_level
455            .load(std::sync::atomic::Ordering::Relaxed);
456
457        assert_eq!(
458            stored,
459            llama_cpp_bindings_sys::GGML_LOG_LEVEL_ERROR.cast_signed()
460        );
461    }
462
463    #[test]
464    fn buffer_non_cont_sets_buffering_flag() {
465        let state = State::new(Module::LlamaCpp, LogOptions::default());
466
467        state.buffer_non_cont(llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO, "partial");
468
469        assert!(
470            state
471                .is_buffering
472                .load(std::sync::atomic::Ordering::Relaxed)
473        );
474
475        let buffer = state
476            .buffered
477            .lock()
478            .unwrap_or_else(std::sync::PoisonError::into_inner);
479
480        assert!(buffer.is_some());
481        let (level, text) = buffer.as_ref().unwrap();
482        assert_eq!(*level, llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO);
483        assert_eq!(text, "partial");
484        drop(buffer);
485    }
486
487    #[test]
488    fn cont_buffered_log_appends_to_existing_buffer() {
489        let state = State::new(Module::LlamaCpp, LogOptions::default());
490
491        state.buffer_non_cont(llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO, "hello ");
492
493        state.cont_buffered_log("world");
494
495        let buffer = state
496            .buffered
497            .lock()
498            .unwrap_or_else(std::sync::PoisonError::into_inner);
499
500        assert!(buffer.is_some());
501        let (_, text) = buffer.as_ref().unwrap();
502        assert_eq!(text, "hello world");
503        drop(buffer);
504    }
505
506    struct Logger {
507        #[allow(unused)]
508        guard: tracing::subscriber::DefaultGuard,
509        logs: Arc<Mutex<Vec<String>>>,
510    }
511
512    #[derive(Clone)]
513    struct VecWriter(Arc<Mutex<Vec<String>>>);
514
515    impl std::io::Write for VecWriter {
516        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
517            let log_line = String::from_utf8_lossy(buf).into_owned();
518            self.0.lock().unwrap().push(log_line);
519
520            Ok(buf.len())
521        }
522
523        fn flush(&mut self) -> std::io::Result<()> {
524            Ok(())
525        }
526    }
527
528    fn create_logger(max_level: tracing::Level) -> Logger {
529        let logs = Arc::new(Mutex::new(vec![]));
530        let writer = VecWriter(logs.clone());
531
532        Logger {
533            guard: tracing_subscriber::fmt()
534                .with_max_level(max_level)
535                .with_ansi(false)
536                .without_time()
537                .with_file(false)
538                .with_line_number(false)
539                .with_level(false)
540                .with_target(false)
541                .with_writer(move || writer.clone())
542                .finish()
543                .set_default(),
544            logs,
545        }
546    }
547
548    #[test]
549    fn cont_disabled_log() {
550        let logger = create_logger(tracing::Level::INFO);
551        let mut log_state = Box::new(State::new(Module::LlamaCpp, LogOptions::default()));
552        let log_ptr =
553            std::ptr::from_mut::<State>(log_state.as_mut()).cast::<std::os::raw::c_void>();
554
555        logs_to_trace(
556            llama_cpp_bindings_sys::GGML_LOG_LEVEL_DEBUG,
557            c"Hello ".as_ptr(),
558            log_ptr,
559        );
560        logs_to_trace(
561            llama_cpp_bindings_sys::GGML_LOG_LEVEL_CONT,
562            c"world\n".as_ptr(),
563            log_ptr,
564        );
565
566        assert!(logger.logs.lock().unwrap().is_empty());
567
568        logs_to_trace(
569            llama_cpp_bindings_sys::GGML_LOG_LEVEL_DEBUG,
570            c"Hello ".as_ptr(),
571            log_ptr,
572        );
573        logs_to_trace(
574            llama_cpp_bindings_sys::GGML_LOG_LEVEL_CONT,
575            c"world".as_ptr(),
576            log_ptr,
577        );
578        logs_to_trace(
579            llama_cpp_bindings_sys::GGML_LOG_LEVEL_CONT,
580            c"\n".as_ptr(),
581            log_ptr,
582        );
583    }
584
585    #[test]
586    fn cont_enabled_log() {
587        let logger = create_logger(tracing::Level::INFO);
588        let mut log_state = Box::new(State::new(Module::LlamaCpp, LogOptions::default()));
589        let log_ptr =
590            std::ptr::from_mut::<State>(log_state.as_mut()).cast::<std::os::raw::c_void>();
591
592        logs_to_trace(
593            llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO,
594            c"Hello ".as_ptr(),
595            log_ptr,
596        );
597        logs_to_trace(
598            llama_cpp_bindings_sys::GGML_LOG_LEVEL_CONT,
599            c"world\n".as_ptr(),
600            log_ptr,
601        );
602
603        // The CONT message carries its own trailing newline, and the flush appends another.
604        assert_eq!(*logger.logs.lock().unwrap(), vec!["Hello world\n\n"]);
605    }
606
607    #[test]
608    fn disabled_logs_are_suppressed() {
609        let logger = create_logger(tracing::Level::DEBUG);
610        let disabled_options = LogOptions::default().with_logs_enabled(false);
611        let mut log_state = Box::new(State::new(Module::LlamaCpp, disabled_options));
612        let log_ptr =
613            std::ptr::from_mut::<State>(log_state.as_mut()).cast::<std::os::raw::c_void>();
614
615        logs_to_trace(
616            llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO,
617            c"Should not appear\n".as_ptr(),
618            log_ptr,
619        );
620        logs_to_trace(
621            llama_cpp_bindings_sys::GGML_LOG_LEVEL_ERROR,
622            c"Also suppressed\n".as_ptr(),
623            log_ptr,
624        );
625
626        assert!(logger.logs.lock().unwrap().is_empty());
627    }
628
629    #[test]
630    fn info_level_log_emitted() {
631        let logger = create_logger(tracing::Level::INFO);
632        let mut log_state = Box::new(State::new(Module::LlamaCpp, LogOptions::default()));
633        let log_ptr =
634            std::ptr::from_mut::<State>(log_state.as_mut()).cast::<std::os::raw::c_void>();
635
636        logs_to_trace(
637            llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO,
638            c"info message\n".as_ptr(),
639            log_ptr,
640        );
641
642        let logs = logger.logs.lock().unwrap();
643        assert_eq!(logs.len(), 1);
644        assert!(logs[0].contains("info message"));
645        drop(logs);
646    }
647
648    #[test]
649    fn warn_level_log_emitted() {
650        let logger = create_logger(tracing::Level::WARN);
651        let mut log_state = Box::new(State::new(Module::LlamaCpp, LogOptions::default()));
652        let log_ptr =
653            std::ptr::from_mut::<State>(log_state.as_mut()).cast::<std::os::raw::c_void>();
654
655        logs_to_trace(
656            llama_cpp_bindings_sys::GGML_LOG_LEVEL_WARN,
657            c"warning message\n".as_ptr(),
658            log_ptr,
659        );
660
661        let logs = logger.logs.lock().unwrap();
662        assert_eq!(logs.len(), 1);
663        assert!(logs[0].contains("warning message"));
664        drop(logs);
665    }
666
667    #[test]
668    fn error_level_log_emitted() {
669        let logger = create_logger(tracing::Level::ERROR);
670        let mut log_state = Box::new(State::new(Module::LlamaCpp, LogOptions::default()));
671        let log_ptr =
672            std::ptr::from_mut::<State>(log_state.as_mut()).cast::<std::os::raw::c_void>();
673
674        logs_to_trace(
675            llama_cpp_bindings_sys::GGML_LOG_LEVEL_ERROR,
676            c"error message\n".as_ptr(),
677            log_ptr,
678        );
679
680        let logs = logger.logs.lock().unwrap();
681        assert_eq!(logs.len(), 1);
682        assert!(logs[0].contains("error message"));
683        drop(logs);
684    }
685
686    #[test]
687    fn debug_level_log_emitted_when_enabled() {
688        let logger = create_logger(tracing::Level::DEBUG);
689        let mut log_state = Box::new(State::new(Module::LlamaCpp, LogOptions::default()));
690        let log_ptr =
691            std::ptr::from_mut::<State>(log_state.as_mut()).cast::<std::os::raw::c_void>();
692
693        logs_to_trace(
694            llama_cpp_bindings_sys::GGML_LOG_LEVEL_DEBUG,
695            c"debug message\n".as_ptr(),
696            log_ptr,
697        );
698
699        let logs = logger.logs.lock().unwrap();
700        assert_eq!(logs.len(), 1);
701        assert!(logs[0].contains("debug message"));
702        drop(logs);
703    }
704
705    #[test]
706    fn submodule_extraction_from_log_text() {
707        let logger = create_logger(tracing::Level::INFO);
708        let mut log_state = Box::new(State::new(Module::LlamaCpp, LogOptions::default()));
709        let log_ptr =
710            std::ptr::from_mut::<State>(log_state.as_mut()).cast::<std::os::raw::c_void>();
711
712        logs_to_trace(
713            llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO,
714            c"sampling: initialized\n".as_ptr(),
715            log_ptr,
716        );
717
718        let logs = logger.logs.lock().unwrap();
719        assert_eq!(logs.len(), 1);
720        assert!(logs[0].contains("initialized"));
721        drop(logs);
722    }
723
724    #[test]
725    fn multi_part_cont_log() {
726        let logger = create_logger(tracing::Level::INFO);
727        let mut log_state = Box::new(State::new(Module::LlamaCpp, LogOptions::default()));
728        let log_ptr =
729            std::ptr::from_mut::<State>(log_state.as_mut()).cast::<std::os::raw::c_void>();
730
731        logs_to_trace(
732            llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO,
733            c"part1 ".as_ptr(),
734            log_ptr,
735        );
736        logs_to_trace(
737            llama_cpp_bindings_sys::GGML_LOG_LEVEL_CONT,
738            c"part2 ".as_ptr(),
739            log_ptr,
740        );
741        logs_to_trace(
742            llama_cpp_bindings_sys::GGML_LOG_LEVEL_CONT,
743            c"part3\n".as_ptr(),
744            log_ptr,
745        );
746
747        let logs = logger.logs.lock().unwrap();
748        assert_eq!(logs.len(), 1);
749        assert!(logs[0].contains("part1 part2 part3"));
750        drop(logs);
751    }
752
753    #[test]
754    fn demote_info_to_debug_suppresses_info_under_info_subscriber() {
755        let logger = create_logger(tracing::Level::INFO);
756        let options = LogOptions::default().with_demote_info_to_debug(true);
757        let mut log_state = Box::new(State::new(Module::LlamaCpp, options));
758        let log_ptr =
759            std::ptr::from_mut::<State>(log_state.as_mut()).cast::<std::os::raw::c_void>();
760
761        logs_to_trace(
762            llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO,
763            c"should be suppressed\n".as_ptr(),
764            log_ptr,
765        );
766
767        assert!(logger.logs.lock().unwrap().is_empty());
768    }
769
770    #[test]
771    fn demote_info_to_debug_emits_info_under_debug_subscriber() {
772        let logger = create_logger(tracing::Level::DEBUG);
773        let options = LogOptions::default().with_demote_info_to_debug(true);
774        let mut log_state = Box::new(State::new(Module::LlamaCpp, options));
775        let log_ptr =
776            std::ptr::from_mut::<State>(log_state.as_mut()).cast::<std::os::raw::c_void>();
777
778        logs_to_trace(
779            llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO,
780            c"visible at debug\n".as_ptr(),
781            log_ptr,
782        );
783
784        let logs = logger.logs.lock().unwrap();
785        assert_eq!(logs.len(), 1);
786        assert!(logs[0].contains("visible at debug"));
787        drop(logs);
788    }
789
790    #[test]
791    fn demote_info_to_debug_preserves_error_under_info_subscriber() {
792        let logger = create_logger(tracing::Level::INFO);
793        let options = LogOptions::default().with_demote_info_to_debug(true);
794        let mut log_state = Box::new(State::new(Module::LlamaCpp, options));
795        let log_ptr =
796            std::ptr::from_mut::<State>(log_state.as_mut()).cast::<std::os::raw::c_void>();
797
798        logs_to_trace(
799            llama_cpp_bindings_sys::GGML_LOG_LEVEL_ERROR,
800            c"error still visible\n".as_ptr(),
801            log_ptr,
802        );
803
804        let logs = logger.logs.lock().unwrap();
805        assert_eq!(logs.len(), 1);
806        assert!(logs[0].contains("error still visible"));
807        drop(logs);
808    }
809
810    #[test]
811    fn demote_info_to_debug_preserves_warn_under_info_subscriber() {
812        let logger = create_logger(tracing::Level::INFO);
813        let options = LogOptions::default().with_demote_info_to_debug(true);
814        let mut log_state = Box::new(State::new(Module::LlamaCpp, options));
815        let log_ptr =
816            std::ptr::from_mut::<State>(log_state.as_mut()).cast::<std::os::raw::c_void>();
817
818        logs_to_trace(
819            llama_cpp_bindings_sys::GGML_LOG_LEVEL_WARN,
820            c"warning still visible\n".as_ptr(),
821            log_ptr,
822        );
823
824        let logs = logger.logs.lock().unwrap();
825        assert_eq!(logs.len(), 1);
826        assert!(logs[0].contains("warning still visible"));
827        drop(logs);
828    }
829
830    #[test]
831    fn emit_non_cont_line_level_none() {
832        let logger = create_logger(tracing::Level::INFO);
833        let state = State::new(Module::LlamaCpp, LogOptions::default());
834
835        state.emit_non_cont_line(
836            llama_cpp_bindings_sys::GGML_LOG_LEVEL_NONE,
837            "none level message\n",
838        );
839
840        let logs = logger.logs.lock().unwrap();
841        assert_eq!(logs.len(), 1);
842        assert!(logs[0].contains("none level message"));
843        drop(logs);
844    }
845
846    #[test]
847    fn emit_non_cont_line_level_none_demoted_to_debug() {
848        let logger = create_logger(tracing::Level::DEBUG);
849        let options = LogOptions::default().with_demote_info_to_debug(true);
850        let state = State::new(Module::LlamaCpp, options);
851
852        state.emit_non_cont_line(
853            llama_cpp_bindings_sys::GGML_LOG_LEVEL_NONE,
854            "demoted none\n",
855        );
856
857        let logs = logger.logs.lock().unwrap();
858        assert_eq!(logs.len(), 1);
859        assert!(logs[0].contains("demoted none"));
860        drop(logs);
861    }
862
863    #[test]
864    fn cont_without_prior_buffer_infers_level() {
865        let _logger = create_logger(tracing::Level::WARN);
866        let state = State::new(Module::LlamaCpp, LogOptions::default());
867
868        state.update_previous_level_for_disabled_log(llama_cpp_bindings_sys::GGML_LOG_LEVEL_WARN);
869        state.cont_buffered_log("orphan text");
870
871        let buffer = state.buffered.lock().unwrap();
872        assert!(buffer.is_some());
873        let (level, text) = buffer.as_ref().unwrap();
874        assert_eq!(*level, llama_cpp_bindings_sys::GGML_LOG_LEVEL_WARN);
875        assert_eq!(text, "orphan text");
876        drop(buffer);
877    }
878
879    #[test]
880    fn emit_non_cont_flushes_stale_buffer() {
881        let _logger = create_logger(tracing::Level::WARN);
882        let state = State::new(Module::LlamaCpp, LogOptions::default());
883
884        state.buffer_non_cont(llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO, "stale");
885
886        state.emit_non_cont_line(llama_cpp_bindings_sys::GGML_LOG_LEVEL_WARN, "new line\n");
887
888        let buffer = state.buffered.lock().unwrap();
889        assert!(buffer.is_none());
890        drop(buffer);
891    }
892
893    #[test]
894    fn buffer_non_cont_replaces_previous_buffer() {
895        let _logger = create_logger(tracing::Level::WARN);
896        let state = State::new(Module::LlamaCpp, LogOptions::default());
897
898        state.buffer_non_cont(llama_cpp_bindings_sys::GGML_LOG_LEVEL_INFO, "first");
899        state.buffer_non_cont(llama_cpp_bindings_sys::GGML_LOG_LEVEL_WARN, "second");
900
901        let buffer = state.buffered.lock().unwrap();
902        let (level, text) = buffer.as_ref().unwrap();
903        assert_eq!(*level, llama_cpp_bindings_sys::GGML_LOG_LEVEL_WARN);
904        assert_eq!(text, "second");
905        drop(buffer);
906    }
907
908    #[test]
909    fn is_enabled_for_cont_uses_previous_level() {
910        let _logger = create_logger(tracing::Level::WARN);
911        let state = State::new(Module::LlamaCpp, LogOptions::default());
912
913        state.update_previous_level_for_disabled_log(llama_cpp_bindings_sys::GGML_LOG_LEVEL_ERROR);
914
915        let enabled = state.is_enabled_for_level(llama_cpp_bindings_sys::GGML_LOG_LEVEL_CONT);
916
917        assert!(enabled);
918    }
919
920    #[test]
921    fn unknown_log_level_emits_warning() {
922        let logger = create_logger(tracing::Level::WARN);
923        let state = State::new(Module::LlamaCpp, LogOptions::default());
924
925        state.emit_non_cont_line(9999, "unknown level message\n");
926
927        let logs = logger.logs.lock().unwrap();
928        assert!(
929            logs.iter()
930                .any(|log_line| log_line.contains("Unknown llama.cpp log level"))
931        );
932        drop(logs);
933    }
934
935    #[test]
936    fn send_logs_to_tracing_initializes_global_states() {
937        use super::{GGML_STATE, LLAMA_STATE, send_logs_to_tracing};
938
939        send_logs_to_tracing(LogOptions::default());
940
941        assert!(LLAMA_STATE.get().is_some());
942        assert!(GGML_STATE.get().is_some());
943    }
944
945    #[test]
946    fn meta_for_level_returns_none_for_unknown_level() {
947        let result = super::meta_for_level(9999);
948
949        assert!(result.is_none());
950    }
951
952    #[test]
953    fn is_enabled_for_level_returns_false_for_none_level() {
954        let _logger = create_logger(tracing::Level::DEBUG);
955        let state = State::new(Module::LlamaCpp, LogOptions::default());
956
957        let enabled = state.is_enabled_for_level(llama_cpp_bindings_sys::GGML_LOG_LEVEL_NONE);
958
959        assert!(!enabled);
960    }
961
962    #[test]
963    fn generate_log_handles_unmapped_level_gracefully() {
964        let _logger = create_logger(tracing::Level::WARN);
965        let state = State::new(Module::LlamaCpp, LogOptions::default());
966
967        state.generate_log(9999, "unmapped level message");
968    }
969
970    #[test]
971    fn emit_non_cont_line_handles_cont_level_gracefully() {
972        let _logger = create_logger(tracing::Level::WARN);
973        let state = State::new(Module::LlamaCpp, LogOptions::default());
974
975        state.emit_non_cont_line(
976            llama_cpp_bindings_sys::GGML_LOG_LEVEL_CONT,
977            "cont passed to non-cont\n",
978        );
979    }
980
981    #[test]
982    fn callsite_metadata_returns_static_metadata() {
983        use tracing_core::callsite::Callsite;
984
985        let debug_meta = super::DEBUG_CS.metadata();
986        let info_meta = super::INFO_CS.metadata();
987        let warn_meta = super::WARN_CS.metadata();
988        let error_meta = super::ERROR_CS.metadata();
989
990        assert_eq!(*debug_meta.level(), tracing_core::Level::DEBUG);
991        assert_eq!(*info_meta.level(), tracing_core::Level::INFO);
992        assert_eq!(*warn_meta.level(), tracing_core::Level::WARN);
993        assert_eq!(*error_meta.level(), tracing_core::Level::ERROR);
994    }
995
996    #[test]
997    fn callsite_set_interest_does_not_panic() {
998        use tracing_core::callsite::Callsite;
999        use tracing_core::subscriber::Interest;
1000
1001        super::DEBUG_CS.set_interest(Interest::always());
1002        super::INFO_CS.set_interest(Interest::never());
1003        super::WARN_CS.set_interest(Interest::sometimes());
1004        super::ERROR_CS.set_interest(Interest::always());
1005    }
1006
1007    #[test]
1008    fn vec_writer_flush_succeeds() {
1009        use std::io::Write;
1010
1011        let mut writer = VecWriter(Arc::new(Mutex::new(vec![])));
1012
1013        writer.flush().unwrap();
1014    }
1015}