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