1use 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
32pub 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
59pub fn setup_tracing(crate_name: &str) -> color_eyre::Result<Option<EyesShutdownHandle>> {
73 TracingConfig::new(crate_name).init()
74}
75
76pub 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 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
176fn 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}