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}