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 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 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 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 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 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 pub fn is_enabled_for_level(&self, level: llama_cpp_bindings_sys::ggml_log_level) -> bool {
307 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 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 !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 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
378pub fn send_logs_to_tracing(options: LogOptions) {
380 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 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 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}