github_mcp/core/logger.rs
1// GitHub v3 REST API MCP server — generated by mcpify. Do not hand-edit.
2
3use tracing_subscriber::Registry;
4use tracing_subscriber::layer::SubscriberExt;
5use tracing_subscriber::util::SubscriberInitExt;
6use tracing_subscriber::{EnvFilter, Layer, fmt};
7
8use super::log_transport::use_pretty_output;
9
10const LOG_LEVEL_ENV_VAR: &str = "GITHUB_MCP_LOG_LEVEL";
11
12/// A type-erased `tracing_subscriber` layer — lets `otel::build_layer`'s
13/// OpenTelemetry bridge layer compose into the same subscriber
14/// `init_logging` installs, without `init_logging` needing to name
15/// `otel`'s concrete layer type.
16pub type BoxedLayer = Box<dyn Layer<Registry> + Send + Sync>;
17
18fn resolve_log_level_from(value: Option<String>) -> String {
19 value.unwrap_or_else(|| "info".to_string())
20}
21
22/// `GITHUB_MCP_LOG_LEVEL`, defaulting to `"info"` when unset.
23pub fn resolve_log_level() -> String {
24 resolve_log_level_from(std::env::var(LOG_LEVEL_ENV_VAR).ok())
25}
26
27/// Installs the process-global structured logger: JSON output (pretty on an
28/// interactive TTY), level from `resolve_log_level()` — mirrors
29/// `targets::typescript`'s `logger.ts`. Correlation IDs are carried as a
30/// `trace_id` span field (see `correlation_context`) rather than pino's
31/// per-record `mixin`, since `tracing` spans are the idiomatic Rust
32/// equivalent of that pattern. Field-level secret redaction isn't automatic
33/// here; call `sanitizer::sanitize` before logging any user- or
34/// API-controlled JSON payload.
35///
36/// `otel_layer` is composed into the same subscriber when tracing export is
37/// enabled (see `otel::build_layer`), since `tracing` only allows a single
38/// global default subscriber — the two layers can't be installed
39/// independently without one call overwriting the other. Call once, at
40/// process startup.
41pub fn init_logging(otel_layer: Option<BoxedLayer>) {
42 let filter = EnvFilter::try_new(resolve_log_level()).unwrap_or_else(|_| EnvFilter::new("info"));
43 // `.with_writer(std::io::stderr)`: the MCP stdio transport (see
44 // `mcp_server::connect_stdio`) speaks JSON-RPC over stdout — any other
45 // writer sharing that stream (tracing's default) corrupts every frame
46 // parsed on the client side. Logs always go to stderr instead.
47 let fmt_layer: BoxedLayer = if use_pretty_output() {
48 fmt::layer().pretty().with_writer(std::io::stderr).boxed()
49 } else {
50 fmt::layer().json().with_writer(std::io::stderr).boxed()
51 };
52
53 // `fmt_layer`/`otel_layer` are boxed as `Layer<Registry>` specifically,
54 // so they must land directly on the bare `registry()` (a single
55 // `Vec<BoxedLayer>` `.with()` call, since `Vec<L>` itself implements
56 // `Layer<S>`) before anything — including the filter — changes the
57 // accumulated subscriber's type; `EnvFilter` implements `Layer<S>`
58 // generically for any `S`, so it can compose on top afterward.
59 let layers: Vec<BoxedLayer> = std::iter::once(fmt_layer).chain(otel_layer).collect();
60
61 tracing_subscriber::registry()
62 .with(layers)
63 .with(filter)
64 .init();
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn defaults_to_info_when_env_var_unset() {
73 assert_eq!(resolve_log_level_from(None), "info");
74 }
75
76 #[test]
77 fn reads_the_configured_level_from_the_env_var() {
78 assert_eq!(resolve_log_level_from(Some("debug".to_string())), "debug");
79 }
80}