github-mcp 0.1.0

GitHub v3 REST API MCP server, generated by mcpify.
Documentation
// GitHub v3 REST API MCP server — generated by mcpify. Do not hand-edit.

use tracing_subscriber::Registry;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, Layer, fmt};

use super::log_transport::use_pretty_output;

const LOG_LEVEL_ENV_VAR: &str = "GITHUB_MCP_LOG_LEVEL";

/// A type-erased `tracing_subscriber` layer — lets `otel::build_layer`'s
/// OpenTelemetry bridge layer compose into the same subscriber
/// `init_logging` installs, without `init_logging` needing to name
/// `otel`'s concrete layer type.
pub type BoxedLayer = Box<dyn Layer<Registry> + Send + Sync>;

/// `GITHUB_MCP_LOG_LEVEL`, defaulting to `"info"` when unset.
pub fn resolve_log_level() -> String {
    std::env::var(LOG_LEVEL_ENV_VAR).unwrap_or_else(|_| "info".to_string())
}

/// Installs the process-global structured logger: JSON output (pretty on an
/// interactive TTY), level from `resolve_log_level()` — mirrors
/// `targets::typescript`'s `logger.ts`. Correlation IDs are carried as a
/// `trace_id` span field (see `correlation_context`) rather than pino's
/// per-record `mixin`, since `tracing` spans are the idiomatic Rust
/// equivalent of that pattern. Field-level secret redaction isn't automatic
/// here; call `sanitizer::sanitize` before logging any user- or
/// API-controlled JSON payload.
///
/// `otel_layer` is composed into the same subscriber when tracing export is
/// enabled (see `otel::build_layer`), since `tracing` only allows a single
/// global default subscriber — the two layers can't be installed
/// independently without one call overwriting the other. Call once, at
/// process startup.
pub fn init_logging(otel_layer: Option<BoxedLayer>) {
    let filter = EnvFilter::try_new(resolve_log_level()).unwrap_or_else(|_| EnvFilter::new("info"));
    // `.with_writer(std::io::stderr)`: the MCP stdio transport (see
    // `mcp_server::connect_stdio`) speaks JSON-RPC over stdout — any other
    // writer sharing that stream (tracing's default) corrupts every frame
    // parsed on the client side. Logs always go to stderr instead.
    let fmt_layer: BoxedLayer = if use_pretty_output() {
        fmt::layer().pretty().with_writer(std::io::stderr).boxed()
    } else {
        fmt::layer().json().with_writer(std::io::stderr).boxed()
    };

    // `fmt_layer`/`otel_layer` are boxed as `Layer<Registry>` specifically,
    // so they must land directly on the bare `registry()` (a single
    // `Vec<BoxedLayer>` `.with()` call, since `Vec<L>` itself implements
    // `Layer<S>`) before anything — including the filter — changes the
    // accumulated subscriber's type; `EnvFilter` implements `Layer<S>`
    // generically for any `S`, so it can compose on top afterward.
    let layers: Vec<BoxedLayer> = std::iter::once(fmt_layer).chain(otel_layer).collect();

    tracing_subscriber::registry()
        .with(layers)
        .with(filter)
        .init();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn defaults_to_info_when_env_var_unset() {
        // SAFETY: test-only env mutation, single-threaded within this test.
        unsafe {
            std::env::remove_var(LOG_LEVEL_ENV_VAR);
        }
        assert_eq!(resolve_log_level(), "info");
    }

    #[test]
    fn reads_the_configured_level_from_the_env_var() {
        // SAFETY: test-only env mutation, single-threaded within this test.
        unsafe {
            std::env::set_var(LOG_LEVEL_ENV_VAR, "debug");
        }
        assert_eq!(resolve_log_level(), "debug");
        unsafe {
            std::env::remove_var(LOG_LEVEL_ENV_VAR);
        }
    }
}