Skip to main content

lean_ctx/core/
logging.rs

1use std::io::IsTerminal;
2
3use tracing::Level;
4use tracing_subscriber::EnvFilter;
5use tracing_subscriber::layer::SubscriberExt;
6use tracing_subscriber::util::SubscriberInitExt;
7
8/// Initialize the tracing subscriber for CLI usage.
9///
10/// Respects `LEAN_CTX_LOG` and `RUST_LOG` environment variables for filter control.
11/// Defaults to `warn` level — keeps CLI output clean.
12pub fn init_logging() {
13    init_with_default("warn");
14}
15
16/// Initialize logging for daemon/MCP mode (stderr, defaults to `info`).
17/// Daemon logs go to a file, so verbosity is fine.
18pub fn init_mcp_logging() {
19    init_with_default("info");
20}
21
22/// The one place a subscriber is installed, for every process mode (CLI,
23/// stdio MCP server, HTTP/streamable server, daemon, proxy).
24///
25/// Two filters apply, and an event has to pass BOTH:
26///
27/// 1. the directives from `LEAN_CTX_LOG` / `RUST_LOG` (or `default`), as
28///    before;
29/// 2. [`carries_no_request_payload`], installed as a second layer on top.
30///
31/// The second is not a directive inside the first, so nothing in the
32/// environment can lift it: an `EnvFilter` picks the most specific matching
33/// directive (`rmcp::service=trace` would beat a later `rmcp=info`), while a
34/// layer's `enabled` is combined with the layers below it by AND.
35fn init_with_default(default: &str) {
36    let filter = std::env::var("LEAN_CTX_LOG")
37        .or_else(|_| std::env::var("RUST_LOG"))
38        .unwrap_or_else(|_| default.to_string());
39
40    let _ = tracing_subscriber::fmt()
41        .with_env_filter(EnvFilter::new(filter))
42        .with_writer(std::io::stderr)
43        .with_ansi(stderr_is_terminal())
44        .finish()
45        .with(tracing_subscriber::filter::filter_fn(|metadata| {
46            carries_no_request_payload(metadata.target(), *metadata.level())
47        }))
48        .try_init();
49}
50
51/// Whether an event of `target` at `level` may be emitted at all.
52///
53/// The MCP library logs whole inbound messages below `INFO`
54/// (`rmcp::service`: "received request" with the request; the streamable
55/// HTTP transport: every message). A request carries the host's `_meta`,
56/// which may hold a presenter capability
57/// (`server::call_identity`), and tool arguments. So every `rmcp` target is
58/// capped at `INFO`, unconditionally.
59///
60/// Cost, accepted: `rmcp` debug and trace logs are not available in any
61/// lean-ctx process, whatever `RUST_LOG` says.
62fn carries_no_request_payload(target: &str, level: Level) -> bool {
63    let rmcp = target == "rmcp" || target.starts_with("rmcp::");
64    !rmcp || level <= Level::INFO
65}
66
67/// Colour escapes only for a person at a terminal: redirected to a file or
68/// captured into an agent's tool result they are noise (#1866).
69fn stderr_is_terminal() -> bool {
70    std::io::stderr().is_terminal()
71}
72
73#[cfg(test)]
74mod tests {
75    use super::{Level, carries_no_request_payload};
76
77    /// The cap is a predicate of its own, applied on top of whatever the
78    /// environment asks for: for `rmcp::service` (and every other `rmcp`
79    /// target) nothing more verbose than `INFO` passes, so the effective
80    /// maximum is `INFO` under `RUST_LOG=trace` too. Other targets are not
81    /// touched.
82    #[test]
83    fn rmcp_targets_are_capped_at_info_whatever_the_environment_says() {
84        for target in [
85            "rmcp",
86            "rmcp::service",
87            "rmcp::transport::streamable_http_server::tower",
88        ] {
89            for passes in [Level::ERROR, Level::WARN, Level::INFO] {
90                assert!(carries_no_request_payload(target, passes), "{target}");
91            }
92            for capped in [Level::DEBUG, Level::TRACE] {
93                assert!(!carries_no_request_payload(target, capped), "{target}");
94            }
95        }
96        for other in ["lean_ctx::server", "rmcpx", "hyper"] {
97            assert!(carries_no_request_payload(other, Level::TRACE), "{other}");
98        }
99    }
100}