1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::env;
4use std::fmt::{self, Display};
5use std::path::PathBuf;
6use std::str::FromStr;
7
8use relay_common::impl_str_serde;
9use sentry::integrations::tracing::EventFilter;
10use sentry::types::Dsn;
11use serde::{Deserialize, Serialize};
12use tracing::level_filters::LevelFilter;
13use tracing_subscriber::{EnvFilter, Layer, prelude::*};
14
15use crate::crash;
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
247fn get_default_filters() -> EnvFilter {
249 let mut env_filter = EnvFilter::new(
251 "INFO,\
252 sqlx=WARN,\
253 tower_http=TRACE,\
254 trust_dns_proto=WARN,\
255 minidump=ERROR,\
256 metrics_exporter_dogstatsd::forwarder::sync=OFF,\
257 ",
258 );
259
260 for name in CRATE_NAMES {
262 env_filter = env_filter.add_directive(format!("{name}=TRACE").parse().unwrap());
263 }
264
265 env_filter
266}
267
268pub unsafe fn init(config: &LogConfig, sentry: &SentryConfig) {
288 if config.enable_backtraces {
289 unsafe {
290 env::set_var("RUST_BACKTRACE", "full");
291 }
292 }
293
294 let subscriber = tracing_subscriber::fmt::layer()
295 .with_writer(std::io::stderr)
296 .with_target(true);
297
298 let format = match (config.format, console::user_attended()) {
299 (LogFormat::Auto, true) | (LogFormat::Pretty, _) => {
300 subscriber.compact().without_time().boxed()
301 }
302 (LogFormat::Auto, false) | (LogFormat::Simplified, _) => {
303 subscriber.with_ansi(false).boxed()
304 }
305 (LogFormat::Json, _) => subscriber
306 .json()
307 .flatten_event(true)
308 .with_current_span(true)
309 .with_span_list(true)
310 .with_file(true)
311 .with_line_number(true)
312 .boxed(),
313 };
314
315 tracing_subscriber::registry()
316 .with(format.with_filter(config.level_filter()))
317 .with(
318 sentry::integrations::tracing::layer().event_filter(|md| match *md.level() {
321 tracing::Level::ERROR | tracing::Level::WARN => {
322 EventFilter::Event | EventFilter::Log
323 }
324 tracing::Level::INFO => EventFilter::Log,
325 tracing::Level::DEBUG | tracing::Level::TRACE => EventFilter::Ignore,
326 }),
327 )
328 .with(match env::var(EnvFilter::DEFAULT_ENV) {
329 Ok(value) => EnvFilter::new(value),
330 Err(_) => get_default_filters(),
331 })
332 .init();
333
334 if let Some(dsn) = sentry.enabled_dsn() {
335 let traces_sample_rate = config.traces_sample_rate;
336 let mut options = sentry::ClientOptions::new()
344 .in_app_include(["relay"])
345 .release(RELEASE)
346 .attach_stacktrace(config.enable_backtraces)
347 .traces_sampler(move |_| traces_sample_rate);
348 options.dsn = Some(dsn.clone());
349 options.environment = sentry.environment.clone();
350 options.server_name = sentry.server_name.clone();
351
352 if let Some(default_tags) = sentry.default_tags.clone() {
355 options = options.before_send(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 #[cfg(feature = "crash-handler")]
367 if let Some(integration) = crash::integration(sentry) {
368 options = options.add_integration(integration);
369 }
370
371 if !crash::is_crash_reporter_process() {
372 crate::info!(
373 release = RELEASE,
374 server_name = sentry.server_name.as_deref(),
375 environment = sentry.environment.as_deref(),
376 traces_sample_rate,
377 "Initialized Sentry client options"
378 );
379 }
380
381 let guard = sentry::init(options);
382
383 std::mem::forget(guard);
385 }
386}