Skip to main content

gqls/
logging.rs

1//! Stderr verbosity, set once from `-q`/`-v` and shared across the CLI.
2//!
3//! Three levels: **quiet** (results + hard errors only), **normal** (status
4//! chatter — which schema, warming, no-matches), **verbose** (adds diagnostics
5//! — cache hits, rq candidates, why the embedding model loaded or fell back).
6//! Status text goes through [`status!`]/[`detail!`]; under the semantic feature
7//! we also route the borrowed `log::` macros here, so `-v` surfaces the
8//! otherwise-silent model-load `debug!`/`warn!` lines from `ae`'s pipeline.
9
10use std::sync::atomic::{AtomicU8, Ordering};
11
12pub const QUIET: u8 = 0;
13pub const NORMAL: u8 = 1;
14pub const VERBOSE: u8 = 2;
15
16static LEVEL: AtomicU8 = AtomicU8::new(NORMAL);
17
18/// Set the global level from the parsed flags (called once at startup). `-v`
19/// and `-q` are mutually exclusive at the clap layer, so at most one is set.
20pub fn init(verbose: bool, quiet: bool) {
21    let level = if verbose {
22        VERBOSE
23    } else if quiet {
24        QUIET
25    } else {
26        NORMAL
27    };
28    LEVEL.store(level, Ordering::Relaxed);
29    #[cfg(feature = "_semantic")]
30    init_log_backend(level);
31}
32
33pub fn level() -> u8 {
34    LEVEL.load(Ordering::Relaxed)
35}
36
37pub fn is_quiet() -> bool {
38    level() == QUIET
39}
40
41pub fn is_verbose() -> bool {
42    level() >= VERBOSE
43}
44
45/// Normal status chatter — printed unless `-q`. Prefixed `gqls:`.
46#[macro_export]
47macro_rules! status {
48    ($($arg:tt)*) => {
49        if $crate::logging::level() >= $crate::logging::NORMAL {
50            eprintln!("gqls: {}", format_args!($($arg)*));
51        }
52    };
53}
54
55/// Verbose-only diagnostics — printed only under `-v`. Prefixed `gqls:`.
56#[macro_export]
57macro_rules! detail {
58    ($($arg:tt)*) => {
59        if $crate::logging::is_verbose() {
60            eprintln!("gqls: {}", format_args!($($arg)*));
61        }
62    };
63}
64
65/// Route the `log::` macros from the borrowed embedding code to stderr, gated by
66/// our level: quiet silences them, normal shows warnings (real failures like a
67/// failed tokenize/inference), verbose shows the debug lines that explain *why*
68/// the ONNX model loaded or fell back to the hash embedder.
69#[cfg(feature = "_semantic")]
70fn init_log_backend(level: u8) {
71    use log::LevelFilter;
72
73    static LOGGER: StderrLogger = StderrLogger;
74    let filter = match level {
75        QUIET => LevelFilter::Off,
76        VERBOSE => LevelFilter::Debug,
77        _ => LevelFilter::Warn,
78    };
79    // Only the first call can install the logger; ignore a repeat (e.g. tests).
80    let _ = log::set_logger(&LOGGER);
81    log::set_max_level(filter);
82}
83
84#[cfg(feature = "_semantic")]
85struct StderrLogger;
86
87#[cfg(feature = "_semantic")]
88impl log::Log for StderrLogger {
89    fn enabled(&self, meta: &log::Metadata) -> bool {
90        // Only our own crate's diagnostics (including the borrowed `ae` embedding
91        // code, which compiles under the `gqls` crate). Without this, `-v` would
92        // also dump ureq/rustls TLS-handshake debug spam when introspecting a URL.
93        let t = meta.target();
94        t == "gqls" || t.starts_with("gqls::")
95    }
96
97    fn log(&self, record: &log::Record) {
98        if self.enabled(record.metadata()) {
99            eprintln!(
100                "gqls: [{}] {}",
101                record.level().as_str().to_ascii_lowercase(),
102                record.args()
103            );
104        }
105    }
106
107    fn flush(&self) {}
108}