Skip to main content

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
18/// `GITHUB_MCP_LOG_LEVEL`, defaulting to `"info"` when unset.
19pub fn resolve_log_level() -> String {
20    std::env::var(LOG_LEVEL_ENV_VAR).unwrap_or_else(|_| "info".to_string())
21}
22
23/// Installs the process-global structured logger: JSON output (pretty on an
24/// interactive TTY), level from `resolve_log_level()` — mirrors
25/// `targets::typescript`'s `logger.ts`. Correlation IDs are carried as a
26/// `trace_id` span field (see `correlation_context`) rather than pino's
27/// per-record `mixin`, since `tracing` spans are the idiomatic Rust
28/// equivalent of that pattern. Field-level secret redaction isn't automatic
29/// here; call `sanitizer::sanitize` before logging any user- or
30/// API-controlled JSON payload.
31///
32/// `otel_layer` is composed into the same subscriber when tracing export is
33/// enabled (see `otel::build_layer`), since `tracing` only allows a single
34/// global default subscriber — the two layers can't be installed
35/// independently without one call overwriting the other. Call once, at
36/// process startup.
37pub fn init_logging(otel_layer: Option<BoxedLayer>) {
38    let filter = EnvFilter::try_new(resolve_log_level()).unwrap_or_else(|_| EnvFilter::new("info"));
39    // `.with_writer(std::io::stderr)`: the MCP stdio transport (see
40    // `mcp_server::connect_stdio`) speaks JSON-RPC over stdout — any other
41    // writer sharing that stream (tracing's default) corrupts every frame
42    // parsed on the client side. Logs always go to stderr instead.
43    let fmt_layer: BoxedLayer = if use_pretty_output() {
44        fmt::layer().pretty().with_writer(std::io::stderr).boxed()
45    } else {
46        fmt::layer().json().with_writer(std::io::stderr).boxed()
47    };
48
49    // `fmt_layer`/`otel_layer` are boxed as `Layer<Registry>` specifically,
50    // so they must land directly on the bare `registry()` (a single
51    // `Vec<BoxedLayer>` `.with()` call, since `Vec<L>` itself implements
52    // `Layer<S>`) before anything — including the filter — changes the
53    // accumulated subscriber's type; `EnvFilter` implements `Layer<S>`
54    // generically for any `S`, so it can compose on top afterward.
55    let layers: Vec<BoxedLayer> = std::iter::once(fmt_layer).chain(otel_layer).collect();
56
57    tracing_subscriber::registry()
58        .with(layers)
59        .with(filter)
60        .init();
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn defaults_to_info_when_env_var_unset() {
69        // SAFETY: test-only env mutation, single-threaded within this test.
70        unsafe {
71            std::env::remove_var(LOG_LEVEL_ENV_VAR);
72        }
73        assert_eq!(resolve_log_level(), "info");
74    }
75
76    #[test]
77    fn reads_the_configured_level_from_the_env_var() {
78        // SAFETY: test-only env mutation, single-threaded within this test.
79        unsafe {
80            std::env::set_var(LOG_LEVEL_ENV_VAR, "debug");
81        }
82        assert_eq!(resolve_log_level(), "debug");
83        unsafe {
84            std::env::remove_var(LOG_LEVEL_ENV_VAR);
85        }
86    }
87}