Skip to main content

ati/core/
logging.rs

1//! Structured logging initialization for ATI.
2//!
3//! - **Proxy mode**: JSON to stderr (Docker/container friendly, machine-parseable)
4//! - **CLI mode**: Compact human-readable to stderr
5//!
6//! Sentry integration is behind the `sentry` cargo feature (off by default).
7
8use tracing_subscriber::layer::SubscriberExt;
9use tracing_subscriber::util::SubscriberInitExt;
10use tracing_subscriber::{fmt, EnvFilter};
11
12/// Controls the log output format.
13pub enum LogMode {
14    /// CLI commands — compact human-readable stderr.
15    Cli,
16    /// Proxy server — structured JSON to stderr.
17    Proxy,
18}
19
20/// Opaque guard type. When the `sentry` feature is enabled this is
21/// `sentry::ClientInitGuard` (must be held for program lifetime).
22/// Otherwise it is `()`.
23#[cfg(feature = "sentry")]
24pub type SentryGuard = sentry::ClientInitGuard;
25#[cfg(not(feature = "sentry"))]
26pub type SentryGuard = ();
27
28/// Initialize the tracing subscriber and (optionally) Sentry.
29///
30/// Call once at program startup, before any `tracing` macros fire.
31/// The returned guard (if `Some`) must be held until program exit so
32/// that pending Sentry events are flushed on drop.
33pub fn init(mode: LogMode, verbose: bool) -> Option<SentryGuard> {
34    let filter = match std::env::var("RUST_LOG") {
35        Ok(val) if !val.is_empty() => EnvFilter::from_default_env(),
36        _ if verbose => EnvFilter::new("debug"),
37        _ => EnvFilter::new("info"),
38    };
39
40    // Init Sentry first (before subscriber) so sentry-tracing layer can be wired in.
41    let sentry_guard = init_sentry();
42
43    // Build the layered subscriber.
44    // The sentry-tracing layer (when enabled) bridges tracing events to Sentry:
45    //   error! → Sentry issue, warn!/info! → breadcrumbs.
46    let registry = tracing_subscriber::registry().with(filter);
47
48    #[cfg(feature = "sentry")]
49    let registry = registry.with(sentry_guard.as_ref().map(|_| sentry_tracing::layer()));
50
51    match mode {
52        LogMode::Proxy => {
53            registry
54                .with(
55                    fmt::layer()
56                        .json()
57                        .flatten_event(true)
58                        .with_writer(std::io::stderr)
59                        .with_target(true)
60                        .with_current_span(false),
61                )
62                .init();
63        }
64        LogMode::Cli => {
65            registry
66                .with(
67                    fmt::layer()
68                        .compact()
69                        .with_writer(std::io::stderr)
70                        .with_target(false),
71                )
72                .init();
73        }
74    }
75
76    // Warn after subscriber is initialized so the message actually appears.
77    #[cfg(not(feature = "sentry"))]
78    if std::env::var("SENTRY_DSN").is_ok() || std::env::var("GREP_SENTRY_DSN").is_ok() {
79        tracing::warn!(
80            "SENTRY_DSN is set but this binary was compiled without the sentry feature — ignoring. \
81             Build with: cargo build --features sentry"
82        );
83    }
84
85    sentry_guard
86}
87
88/// Initialize Sentry if a DSN is configured. Returns `None` when Sentry is
89/// disabled (no DSN, or feature not compiled in).
90fn init_sentry() -> Option<SentryGuard> {
91    #[cfg(feature = "sentry")]
92    {
93        let dsn = std::env::var("GREP_SENTRY_DSN")
94            .or_else(|_| std::env::var("SENTRY_DSN"))
95            .ok()?;
96
97        let environment =
98            std::env::var("ENVIRONMENT_TIER").unwrap_or_else(|_| "development".into());
99
100        // Only send to Sentry in production/staging/demo — skip in development
101        match environment.as_str() {
102            "production" | "staging" | "demo" => {}
103            _ => {
104                tracing::debug!(environment = %environment, "sentry disabled for this environment");
105                return None;
106            }
107        }
108
109        let service = std::env::var("SERVICE_NAME").unwrap_or_else(|_| "ati-proxy".into());
110
111        let sample_rate = match environment.as_str() {
112            "production" => 0.25,
113            "staging" => 0.5,
114            _ => 1.0,
115        };
116
117        let guard = sentry::init((
118            dsn,
119            sentry::ClientOptions {
120                release: Some(env!("CARGO_PKG_VERSION").into()),
121                environment: Some(environment.into()),
122                server_name: Some(service.into()),
123                traces_sample_rate: sample_rate,
124                attach_stacktrace: true,
125                send_default_pii: false,
126                ..Default::default()
127            },
128        ));
129
130        if guard.is_enabled() {
131            Some(guard)
132        } else {
133            None
134        }
135    }
136
137    #[cfg(not(feature = "sentry"))]
138    {
139        None
140    }
141}