Skip to main content

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    TracingConfig::new(crate_name).init()
74}
75
76/// Tracing setup with an optional boot-stable Eyes process identity.
77/// Use the same identity in the boot manifest and heartbeat configuration.
78pub struct TracingConfig<'a> {
79    crate_name: &'a str,
80    process: Option<eyes_subscriber::ProcessIdentity>,
81}
82
83impl<'a> TracingConfig<'a> {
84    #[must_use]
85    pub fn new(crate_name: &'a str) -> Self {
86        Self {
87            crate_name,
88            process: None,
89        }
90    }
91
92    #[must_use]
93    pub fn process(mut self, identity: eyes_subscriber::ProcessIdentity) -> Self {
94        self.process = Some(identity);
95        self
96    }
97
98    pub fn init(self) -> color_eyre::Result<Option<EyesShutdownHandle>> {
99        initialize_tracing(self.crate_name, self.process.as_ref())
100    }
101}
102
103fn initialize_tracing(
104    crate_name: &str,
105    process: Option<&eyes_subscriber::ProcessIdentity>,
106) -> color_eyre::Result<Option<EyesShutdownHandle>> {
107    let rust_log = std::env::var("RUST_LOG")
108        .unwrap_or_else(|_| format!("info,{crate_name}=trace,tower_http=debug,serenity=error"));
109
110    let env_filter = EnvFilter::builder().parse(&rust_log).wrap_err_with(|| {
111        color_eyre::eyre::eyre!("Couldn't create env filter from {}", rust_log)
112    })?;
113
114    let opentelemetry_layer = if let Ok(honeycomb_key) = std::env::var("HONEYCOMB_API_KEY") {
115        let mut map = HashMap::<String, String>::new();
116        map.insert("x-honeycomb-team".to_string(), honeycomb_key);
117        map.insert("x-honeycomb-dataset".to_string(), "coreyja.com".to_string());
118
119        let tracer = opentelemetry_otlp::new_pipeline()
120            .tracing()
121            .with_exporter(
122                opentelemetry_otlp::new_exporter()
123                    .http()
124                    .with_endpoint("https://api.honeycomb.io/v1/traces")
125                    .with_timeout(Duration::from_secs(3))
126                    .with_headers(map),
127            )
128            .install_batch(opentelemetry_sdk::runtime::Tokio)?;
129
130        let opentelemetry_layer = OpenTelemetryLayer::new(tracer);
131        println!("Honeycomb layer configured");
132
133        Some(opentelemetry_layer)
134    } else {
135        println!("Skipping Honeycomb layer");
136
137        None
138    };
139
140    // Setup Eyes layer if configured
141    let (eyes_layer, eyes_shutdown_handle) = setup_eyes_layer(process)?;
142
143    let stdout_layer = if std::env::var("JSON_LOGS").is_ok() {
144        println!("Logging to STDOUT as JSON");
145
146        tracing_subscriber::fmt::layer()
147            .json()
148            .with_current_span(true)
149            .boxed()
150    } else {
151        let hierarchical = HierarchicalLayer::default()
152            .with_writer(std::io::stdout)
153            .with_indent_lines(true)
154            .with_indent_amount(2)
155            .with_thread_names(true)
156            .with_thread_ids(true)
157            .with_verbose_exit(true)
158            .with_verbose_entry(true)
159            .with_targets(true);
160
161        println!("Logging to STDOUT as hierarchical");
162
163        hierarchical.boxed()
164    };
165
166    Registry::default()
167        .with(stdout_layer)
168        .with(opentelemetry_layer)
169        .with(eyes_layer)
170        .with(env_filter)
171        .try_init()?;
172
173    Ok(eyes_shutdown_handle)
174}
175
176/// Sets up the Eyes tracing layer if `EYES_ORG_ID` and `EYES_APP_ID` are configured.
177fn setup_eyes_layer(
178    process: Option<&eyes_subscriber::ProcessIdentity>,
179) -> color_eyre::Result<(Option<EyesLayer>, Option<EyesShutdownHandle>)> {
180    let org_id = std::env::var("EYES_ORG_ID").ok();
181    let app_id = std::env::var("EYES_APP_ID").ok();
182
183    match (org_id, app_id) {
184        (Some(org_id_str), Some(app_id_str)) => {
185            let org_id = Uuid::parse_str(&org_id_str)
186                .wrap_err_with(|| format!("Invalid EYES_ORG_ID: {org_id_str}"))?;
187            let app_id = Uuid::parse_str(&app_id_str)
188                .wrap_err_with(|| format!("Invalid EYES_APP_ID: {app_id_str}"))?;
189
190            let (mut builder, transport) =
191                EyesSubscriberBuilder::from_env_with_transport(org_id, app_id)
192                    .wrap_err("Failed to build Eyes subscriber")?;
193            if let Some(process) = process {
194                builder = builder.with_process_instance_id(process.instance_id());
195            }
196            let (layer, shutdown_handle) = builder.build_with_transport(transport);
197
198            let eyes_url = std::env::var("EYES_URL")
199                .unwrap_or_else(|_| "https://eyes.coreyja.com".to_string());
200            println!("Eyes layer configured (org: {org_id}, app: {app_id}, url: {eyes_url})");
201
202            Ok((Some(layer), Some(shutdown_handle)))
203        }
204        (Some(_), None) => {
205            println!("Skipping Eyes layer: EYES_ORG_ID set but EYES_APP_ID missing");
206            Ok((None, None))
207        }
208        (None, Some(_)) => {
209            println!("Skipping Eyes layer: EYES_APP_ID set but EYES_ORG_ID missing");
210            Ok((None, None))
211        }
212        (None, None) => {
213            println!("Skipping Eyes layer");
214            Ok((None, None))
215        }
216    }
217}