cja/
setup.rs

1//! Application setup: Sentry error tracking and tracing/observability initialization.
2//!
3//! Call [`setup_sentry`] before building the Tokio runtime, and [`setup_tracing`]
4//! inside the async context. The tracing function returns an optional
5//! [`EyesShutdownHandle`] — keep it alive in your `main` scope:
6//!
7//! ```rust,ignore
8//! let _sentry_guard = setup_sentry();
9//! let _eyes_handle = setup_tracing("my-app")?;
10//! ```
11//!
12//! # Supported Backends
13//!
14//! - **Console**: Tree-formatted logs (default) or JSON with `JSON_LOGS` env var
15//! - **Sentry**: Error tracking via `SENTRY_DSN`
16//! - **Honeycomb**: OpenTelemetry export via `HONEYCOMB_API_KEY`
17//! - **Eyes**: Distributed tracing via `EYES_ORG_ID` + `EYES_APP_ID` (both required)
18
19use std::{collections::HashMap, time::Duration};
20
21use color_eyre::eyre::Context;
22use eyes_subscriber::{EyesLayer, EyesSubscriberBuilder};
23use opentelemetry_otlp::WithExportConfig;
24use sentry::ClientInitGuard;
25use tracing_opentelemetry::OpenTelemetryLayer;
26use tracing_subscriber::{
27    EnvFilter, Layer as _, Registry, layer::SubscriberExt as _, util::SubscriberInitExt as _,
28};
29use tracing_tree::HierarchicalLayer;
30use uuid::Uuid;
31
32// Re-export for consumers
33pub use eyes_subscriber::EyesShutdownHandle;
34
35pub fn setup_sentry() -> Option<ClientInitGuard> {
36    let git_commit: Option<std::borrow::Cow<_>> =
37        option_env!("VERGEN_GIT_SHA").map(std::convert::Into::into);
38    let release_name =
39        git_commit.unwrap_or_else(|| sentry::release_name!().unwrap_or_else(|| "dev".into()));
40
41    if let Ok(sentry_dsn) = std::env::var("SENTRY_DSN") {
42        println!("Sentry enabled");
43
44        Some(sentry::init((
45            sentry_dsn,
46            sentry::ClientOptions {
47                traces_sample_rate: 0.5,
48                release: Some(release_name),
49                ..Default::default()
50            },
51        )))
52    } else {
53        println!("Sentry not configured in this environment");
54
55        None
56    }
57}
58
59/// Sets up tracing with optional Eyes, Honeycomb, and stdout layers.
60///
61/// Returns an optional `EyesShutdownHandle` that should be used for graceful shutdown
62/// when Eyes is configured (via `EYES_ORG_ID` and `EYES_APP_ID` environment variables).
63///
64/// # Environment Variables
65///
66/// - `RUST_LOG`: Log filter (defaults to `info,{crate_name}=trace,tower_http=debug,serenity=error`)
67/// - `JSON_LOGS`: If set, outputs JSON logs instead of hierarchical
68/// - `HONEYCOMB_API_KEY`: Enables Honeycomb tracing
69/// - `EYES_ORG_ID`: Eyes organization ID (UUID)
70/// - `EYES_APP_ID`: Eyes application ID (UUID)
71/// - `EYES_URL`: Eyes server URL (defaults to `https://eyes.coreyja.com`)
72pub fn setup_tracing(crate_name: &str) -> color_eyre::Result<Option<EyesShutdownHandle>> {
73    let rust_log = std::env::var("RUST_LOG")
74        .unwrap_or_else(|_| format!("info,{crate_name}=trace,tower_http=debug,serenity=error"));
75
76    let env_filter = EnvFilter::builder().parse(&rust_log).wrap_err_with(|| {
77        color_eyre::eyre::eyre!("Couldn't create env filter from {}", rust_log)
78    })?;
79
80    let opentelemetry_layer = if let Ok(honeycomb_key) = std::env::var("HONEYCOMB_API_KEY") {
81        let mut map = HashMap::<String, String>::new();
82        map.insert("x-honeycomb-team".to_string(), honeycomb_key);
83        map.insert("x-honeycomb-dataset".to_string(), "coreyja.com".to_string());
84
85        let tracer = opentelemetry_otlp::new_pipeline()
86            .tracing()
87            .with_exporter(
88                opentelemetry_otlp::new_exporter()
89                    .http()
90                    .with_endpoint("https://api.honeycomb.io/v1/traces")
91                    .with_timeout(Duration::from_secs(3))
92                    .with_headers(map),
93            )
94            .install_batch(opentelemetry_sdk::runtime::Tokio)?;
95
96        let opentelemetry_layer = OpenTelemetryLayer::new(tracer);
97        println!("Honeycomb layer configured");
98
99        Some(opentelemetry_layer)
100    } else {
101        println!("Skipping Honeycomb layer");
102
103        None
104    };
105
106    // Setup Eyes layer if configured
107    let (eyes_layer, eyes_shutdown_handle) = setup_eyes_layer()?;
108
109    let stdout_layer = if std::env::var("JSON_LOGS").is_ok() {
110        println!("Logging to STDOUT as JSON");
111
112        tracing_subscriber::fmt::layer()
113            .json()
114            .with_current_span(true)
115            .boxed()
116    } else {
117        let hierarchical = HierarchicalLayer::default()
118            .with_writer(std::io::stdout)
119            .with_indent_lines(true)
120            .with_indent_amount(2)
121            .with_thread_names(true)
122            .with_thread_ids(true)
123            .with_verbose_exit(true)
124            .with_verbose_entry(true)
125            .with_targets(true);
126
127        println!("Logging to STDOUT as hierarchical");
128
129        hierarchical.boxed()
130    };
131
132    Registry::default()
133        .with(stdout_layer)
134        .with(opentelemetry_layer)
135        .with(eyes_layer)
136        .with(env_filter)
137        .try_init()?;
138
139    Ok(eyes_shutdown_handle)
140}
141
142/// Sets up the Eyes tracing layer if `EYES_ORG_ID` and `EYES_APP_ID` are configured.
143fn setup_eyes_layer() -> color_eyre::Result<(Option<EyesLayer>, Option<EyesShutdownHandle>)> {
144    let org_id = std::env::var("EYES_ORG_ID").ok();
145    let app_id = std::env::var("EYES_APP_ID").ok();
146
147    match (org_id, app_id) {
148        (Some(org_id_str), Some(app_id_str)) => {
149            let org_id = Uuid::parse_str(&org_id_str)
150                .wrap_err_with(|| format!("Invalid EYES_ORG_ID: {org_id_str}"))?;
151            let app_id = Uuid::parse_str(&app_id_str)
152                .wrap_err_with(|| format!("Invalid EYES_APP_ID: {app_id_str}"))?;
153
154            let (layer, shutdown_handle) = EyesSubscriberBuilder::build_from_env(org_id, app_id)
155                .wrap_err("Failed to build Eyes subscriber")?;
156
157            let eyes_url = std::env::var("EYES_URL")
158                .unwrap_or_else(|_| "https://eyes.coreyja.com".to_string());
159            println!("Eyes layer configured (org: {org_id}, app: {app_id}, url: {eyes_url})");
160
161            Ok((Some(layer), Some(shutdown_handle)))
162        }
163        (Some(_), None) => {
164            println!("Skipping Eyes layer: EYES_ORG_ID set but EYES_APP_ID missing");
165            Ok((None, None))
166        }
167        (None, Some(_)) => {
168            println!("Skipping Eyes layer: EYES_APP_ID set but EYES_ORG_ID missing");
169            Ok((None, None))
170        }
171        (None, None) => {
172            println!("Skipping Eyes layer");
173            Ok((None, None))
174        }
175    }
176}