1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::env;
4use std::fmt::{self, Display};
5use std::path::PathBuf;
6use std::str::FromStr;
7use std::sync::Arc;
8
9use relay_common::impl_str_serde;
10use sentry::types::Dsn;
11use sentry::{TracesSampler, TransactionContext};
12use serde::{Deserialize, Serialize};
13use tracing::level_filters::LevelFilter;
14use tracing_subscriber::{prelude::*, EnvFilter, Layer};
15
16const RELEASE: &str = std::env!("RELAY_RELEASE");
18
19include!(concat!(env!("OUT_DIR"), "/constants.gen.rs"));
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)]
24#[serde(rename_all = "lowercase")]
25pub enum LogFormat {
26 Auto,
30
31 Pretty,
37
38 Simplified,
44
45 Json,
51}
52
53#[derive(Clone, Debug)]
55pub struct FormatParseError(String);
56
57impl Display for FormatParseError {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 write!(
60 f,
61 r#"error parsing "{}" as format: expected one of "auto", "pretty", "simplified", "json""#,
62 self.0
63 )
64 }
65}
66
67impl FromStr for LogFormat {
68 type Err = FormatParseError;
69
70 fn from_str(s: &str) -> Result<Self, Self::Err> {
71 let result = match s {
72 "" => LogFormat::Auto,
73 s if s.eq_ignore_ascii_case("auto") => LogFormat::Auto,
74 s if s.eq_ignore_ascii_case("pretty") => LogFormat::Pretty,
75 s if s.eq_ignore_ascii_case("simplified") => LogFormat::Simplified,
76 s if s.eq_ignore_ascii_case("json") => LogFormat::Json,
77 s => return Err(FormatParseError(s.into())),
78 };
79
80 Ok(result)
81 }
82}
83
84impl std::error::Error for FormatParseError {}
85
86#[derive(Clone, Debug)]
88pub struct LevelParseError(String);
89
90impl Display for LevelParseError {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 write!(
93 f,
94 r#"error parsing "{}" as level: expected one of "error", "warn", "info", "debug", "trace", "off""#,
95 self.0
96 )
97 }
98}
99
100#[derive(Clone, Copy, Debug)]
101pub enum Level {
102 Error,
103 Warn,
104 Info,
105 Debug,
106 Trace,
107 Off,
108}
109
110impl_str_serde!(Level, "The logging level.");
111
112impl Level {
113 pub const fn level_filter(&self) -> LevelFilter {
115 match self {
116 Level::Error => LevelFilter::ERROR,
117 Level::Warn => LevelFilter::WARN,
118 Level::Info => LevelFilter::INFO,
119 Level::Debug => LevelFilter::DEBUG,
120 Level::Trace => LevelFilter::TRACE,
121 Level::Off => LevelFilter::OFF,
122 }
123 }
124}
125
126impl Display for Level {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 write!(f, "{}", format!("{:?}", self).to_lowercase())
129 }
130}
131
132impl FromStr for Level {
133 type Err = LevelParseError;
134
135 fn from_str(s: &str) -> Result<Self, Self::Err> {
136 let result = match s {
137 "" => Level::Error,
138 s if s.eq_ignore_ascii_case("error") => Level::Error,
139 s if s.eq_ignore_ascii_case("warn") => Level::Warn,
140 s if s.eq_ignore_ascii_case("info") => Level::Info,
141 s if s.eq_ignore_ascii_case("debug") => Level::Debug,
142 s if s.eq_ignore_ascii_case("trace") => Level::Trace,
143 s if s.eq_ignore_ascii_case("off") => Level::Off,
144 s => return Err(LevelParseError(s.into())),
145 };
146
147 Ok(result)
148 }
149}
150
151impl std::error::Error for LevelParseError {}
152
153#[derive(Clone, Debug, Deserialize, Serialize)]
155#[serde(default)]
156pub struct LogConfig {
157 pub level: Level,
159
160 pub format: LogFormat,
164
165 pub enable_backtraces: bool,
169
170 pub traces_sample_rate: f32,
174}
175
176impl LogConfig {
177 pub const fn level_filter(&self) -> LevelFilter {
179 self.level.level_filter()
180 }
181}
182
183impl Default for LogConfig {
184 fn default() -> Self {
185 Self {
186 level: Level::Info,
187 format: LogFormat::Auto,
188 enable_backtraces: false,
189 #[cfg(debug_assertions)]
190 traces_sample_rate: 1.0,
191 #[cfg(not(debug_assertions))]
192 traces_sample_rate: 0.0,
193 }
194 }
195}
196
197#[derive(Clone, Debug, Deserialize, Serialize)]
199#[serde(default)]
200pub struct SentryConfig {
201 pub dsn: Option<Dsn>,
203
204 pub enabled: bool,
206
207 pub environment: Option<Cow<'static, str>>,
209
210 pub server_name: Option<Cow<'static, str>>,
215
216 pub default_tags: Option<BTreeMap<String, String>>,
218
219 pub _crash_db: Option<PathBuf>,
222}
223
224impl SentryConfig {
225 pub fn enabled_dsn(&self) -> Option<&Dsn> {
227 self.dsn.as_ref().filter(|_| self.enabled)
228 }
229}
230
231impl Default for SentryConfig {
232 fn default() -> Self {
233 Self {
234 dsn: "https://0cc4a37e5aab4da58366266a87a95740@sentry.io/1269704"
235 .parse()
236 .ok(),
237 enabled: false,
238 environment: None,
239 server_name: None,
240 default_tags: None,
241 _crash_db: None,
242 }
243 }
244}
245
246#[cfg(feature = "crash-handler")]
248fn capture_native_envelope(data: &[u8]) {
249 if let Some(client) = sentry::Hub::main().client() {
250 match sentry::Envelope::from_bytes_raw(data.to_owned()) {
251 Ok(envelope) => client.send_envelope(envelope),
252 Err(error) => {
253 let error = &error as &dyn std::error::Error;
254 crate::error!(error, "failed to capture crash")
255 }
256 }
257 } else {
258 crate::error!("failed to capture crash: no sentry client registered");
259 }
260}
261
262fn get_default_filters() -> EnvFilter {
264 let mut env_filter = EnvFilter::new(
266 "INFO,\
267 sqlx=WARN,\
268 tower_http=TRACE,\
269 trust_dns_proto=WARN,\
270 ",
271 );
272
273 for name in CRATE_NAMES {
275 env_filter = env_filter.add_directive(format!("{name}=TRACE").parse().unwrap());
276 }
277
278 env_filter
279}
280
281pub fn init(config: &LogConfig, sentry: &SentryConfig) {
296 if config.enable_backtraces {
297 env::set_var("RUST_BACKTRACE", "full");
298 }
299
300 let subscriber = tracing_subscriber::fmt::layer()
301 .with_writer(std::io::stderr)
302 .with_target(true);
303
304 let format = match (config.format, console::user_attended()) {
305 (LogFormat::Auto, true) | (LogFormat::Pretty, _) => {
306 subscriber.compact().without_time().boxed()
307 }
308 (LogFormat::Auto, false) | (LogFormat::Simplified, _) => {
309 subscriber.with_ansi(false).boxed()
310 }
311 (LogFormat::Json, _) => subscriber
312 .json()
313 .flatten_event(true)
314 .with_current_span(true)
315 .with_span_list(true)
316 .with_file(true)
317 .with_line_number(true)
318 .boxed(),
319 };
320
321 tracing_subscriber::registry()
322 .with(format.with_filter(config.level_filter()))
323 .with(sentry::integrations::tracing::layer())
324 .with(match env::var(EnvFilter::DEFAULT_ENV) {
325 Ok(value) => EnvFilter::new(value),
326 Err(_) => get_default_filters(),
327 })
328 .init();
329
330 if let Some(dsn) = sentry.enabled_dsn() {
331 let traces_sample_rate = config.traces_sample_rate;
332 let traces_sampler =
340 Some(Arc::new(move |_: &TransactionContext| traces_sample_rate) as Arc<TracesSampler>);
341 let mut options = sentry::ClientOptions {
342 dsn: Some(dsn).cloned(),
343 in_app_include: vec!["relay"],
344 release: Some(RELEASE.into()),
345 attach_stacktrace: config.enable_backtraces,
346 environment: sentry.environment.clone(),
347 server_name: sentry.server_name.clone(),
348 traces_sampler,
349 ..Default::default()
350 };
351
352 if let Some(default_tags) = sentry.default_tags.clone() {
355 options.before_send = Some(Arc::new(move |mut event| {
357 let previous_event_tags = std::mem::replace(&mut event.tags, default_tags.clone());
359 event.tags.extend(previous_event_tags);
360 Some(event)
361 }));
362 }
363
364 crate::info!(
365 release = RELEASE,
366 server_name = sentry.server_name.as_deref(),
367 environment = sentry.environment.as_deref(),
368 traces_sample_rate,
369 "Initialized Sentry client options"
370 );
371
372 let guard = sentry::init(options);
373
374 std::mem::forget(guard);
376 }
377
378 #[cfg(feature = "crash-handler")]
381 {
382 if let Some(dsn) = sentry.enabled_dsn().map(|d| d.to_string()) {
383 if let Some(db) = sentry._crash_db.as_deref() {
384 crate::info!("initializing crash handler in {}", db.display());
385 relay_crash::CrashHandler::new(dsn.as_str(), db)
386 .transport(capture_native_envelope)
387 .release(Some(RELEASE))
388 .environment(sentry.environment.as_deref())
389 .install();
390 }
391 }
392 }
393}