relay_log/
setup.rs

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
17/// The full release name including the Relay version and SHA.
18const RELEASE: &str = std::env!("RELAY_RELEASE");
19
20// Import CRATE_NAMES, which lists all crates in the workspace.
21include!(concat!(env!("OUT_DIR"), "/constants.gen.rs"));
22
23/// Controls the log format.
24#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)]
25#[serde(rename_all = "lowercase")]
26pub enum LogFormat {
27    /// Auto detect the best format.
28    ///
29    /// This chooses [`LogFormat::Pretty`] for TTY, otherwise [`LogFormat::Simplified`].
30    Auto,
31
32    /// Pretty printing with colors.
33    ///
34    /// ```text
35    ///  INFO  relay::setup > relay mode: managed
36    /// ```
37    Pretty,
38
39    /// Simplified plain text output.
40    ///
41    /// ```text
42    /// 2020-12-04T12:10:32Z [relay::setup] INFO: relay mode: managed
43    /// ```
44    Simplified,
45
46    /// Dump out JSON lines.
47    ///
48    /// ```text
49    /// {"timestamp":"2020-12-04T12:11:08.729716Z","level":"INFO","logger":"relay::setup","message":"  relay mode: managed","module_path":"relay::setup","filename":"relay/src/setup.rs","lineno":31}
50    /// ```
51    Json,
52}
53
54/// The logging format parse error.
55#[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/// The logging level parse error.
88#[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    /// Returns the tracing [`LevelFilter`].
115    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/// Controls the logging system.
155#[derive(Clone, Debug, Deserialize, Serialize)]
156#[serde(default)]
157pub struct LogConfig {
158    /// The log level for Relay.
159    pub level: Level,
160
161    /// Controls the log output format.
162    ///
163    /// Defaults to [`LogFormat::Auto`], which detects the best format based on the TTY.
164    pub format: LogFormat,
165
166    /// When set to `true`, backtraces are forced on.
167    ///
168    /// Otherwise, backtraces can be enabled by setting the `RUST_BACKTRACE` variable to `full`.
169    pub enable_backtraces: bool,
170
171    /// Sets the trace sample rate for performance monitoring.
172    ///
173    /// Defaults to `0.0` for release builds and `1.0` for local development builds.
174    pub traces_sample_rate: f32,
175}
176
177impl LogConfig {
178    /// Returns the tracing [`LevelFilter`].
179    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/// Controls internal reporting to Sentry.
199#[derive(Clone, Debug, Deserialize, Serialize)]
200#[serde(default)]
201pub struct SentryConfig {
202    /// The [`DSN`](sentry::types::Dsn) specifying the Project to report to.
203    pub dsn: Option<Dsn>,
204
205    /// Enables reporting to Sentry.
206    pub enabled: bool,
207
208    /// Sets the environment for this service.
209    pub environment: Option<Cow<'static, str>>,
210
211    /// Sets the server name for this service.
212    ///
213    /// This is overridden by the `RELAY_SERVER_NAME`
214    /// environment variable.
215    pub server_name: Option<Cow<'static, str>>,
216
217    /// Add defaults tags to the events emitted by Relay
218    pub default_tags: Option<BTreeMap<String, String>>,
219
220    /// Internal. Enables crash handling and sets the absolute path to where minidumps should be
221    /// cached on disk. The path is created if it doesn't exist. Path must be UTF-8.
222    pub _crash_db: Option<PathBuf>,
223}
224
225impl SentryConfig {
226    /// Returns a reference to the [`DSN`](sentry::types::Dsn) if Sentry is enabled.
227    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/// Captures an envelope from the native crash reporter using the main Sentry SDK.
248#[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
263/// Configures the given log level for all of Relay's crates.
264fn get_default_filters() -> EnvFilter {
265    // Configure INFO as default, except for crates that are very spammy on INFO level.
266    let mut env_filter = EnvFilter::new(
267        "INFO,\
268        sqlx=WARN,\
269        tower_http=TRACE,\
270        trust_dns_proto=WARN,\
271        minidump=ERROR,\
272        metrics_exporter_dogstatsd::forwarder::sync=OFF,\
273        ",
274    );
275
276    // Add all internal modules with maximum log-level.
277    for name in CRATE_NAMES {
278        env_filter = env_filter.add_directive(format!("{name}=TRACE").parse().unwrap());
279    }
280
281    env_filter
282}
283
284/// Initialize the logging system and reporting to Sentry.
285///
286/// # Safety
287///
288/// The function is not safe to be called from a multi-threaded program,
289/// due to modifications of environment variables.
290///
291/// # Example
292///
293/// ```
294/// let log_config = relay_log::LogConfig {
295///     enable_backtraces: true,
296///     ..Default::default()
297/// };
298///
299/// let sentry_config = relay_log::SentryConfig::default();
300///
301/// unsafe { relay_log::init(&log_config, &sentry_config) };
302/// ```
303pub unsafe fn init(config: &LogConfig, sentry: &SentryConfig) {
304    if config.enable_backtraces {
305        unsafe {
306            env::set_var("RUST_BACKTRACE", "full");
307        }
308    }
309
310    let subscriber = tracing_subscriber::fmt::layer()
311        .with_writer(std::io::stderr)
312        .with_target(true);
313
314    let format = match (config.format, console::user_attended()) {
315        (LogFormat::Auto, true) | (LogFormat::Pretty, _) => {
316            subscriber.compact().without_time().boxed()
317        }
318        (LogFormat::Auto, false) | (LogFormat::Simplified, _) => {
319            subscriber.with_ansi(false).boxed()
320        }
321        (LogFormat::Json, _) => subscriber
322            .json()
323            .flatten_event(true)
324            .with_current_span(true)
325            .with_span_list(true)
326            .with_file(true)
327            .with_line_number(true)
328            .boxed(),
329    };
330
331    tracing_subscriber::registry()
332        .with(format.with_filter(config.level_filter()))
333        .with(
334            // Same as the default filter, except it converts warnings into events
335            // and also sends everything at or above INFO as logs instead of breadcrumbs.
336            sentry::integrations::tracing::layer().event_filter(|md| match *md.level() {
337                tracing::Level::ERROR | tracing::Level::WARN => {
338                    EventFilter::Event | EventFilter::Log
339                }
340                tracing::Level::INFO => EventFilter::Log,
341                tracing::Level::DEBUG | tracing::Level::TRACE => EventFilter::Ignore,
342            }),
343        )
344        .with(match env::var(EnvFilter::DEFAULT_ENV) {
345            Ok(value) => EnvFilter::new(value),
346            Err(_) => get_default_filters(),
347        })
348        .init();
349
350    if let Some(dsn) = sentry.enabled_dsn() {
351        let traces_sample_rate = config.traces_sample_rate;
352        // We're explicitly setting a `traces_sampler` here to circumvent trace
353        // propagation. A trace sampler that always just returns the constant
354        // `traces_sample_rate` is equivalent to using the `traces_sample_rate`
355        // directly, except it doesn't take into account whether the context
356        // was previously sampled. We don't want to take that into account because
357        // SDKs send headers with their envelopes that erroneously cause us to
358        // sample transactions.
359        let traces_sampler =
360            Some(Arc::new(move |_: &TransactionContext| traces_sample_rate) as Arc<TracesSampler>);
361        let mut options = sentry::ClientOptions {
362            dsn: Some(dsn).cloned(),
363            in_app_include: vec!["relay"],
364            release: Some(RELEASE.into()),
365            attach_stacktrace: config.enable_backtraces,
366            environment: sentry.environment.clone(),
367            server_name: sentry.server_name.clone(),
368            traces_sampler,
369            enable_logs: true,
370            ..Default::default()
371        };
372
373        // If `default_tags` is set in Sentry configuration install the `before_send` hook
374        // in order to inject said tags into each event
375        if let Some(default_tags) = sentry.default_tags.clone() {
376            // Install hook
377            options.before_send = Some(Arc::new(move |mut event| {
378                // Extend `event.tags` with `default_tags` without replacing tags already present
379                let previous_event_tags = std::mem::replace(&mut event.tags, default_tags.clone());
380                event.tags.extend(previous_event_tags);
381                Some(event)
382            }));
383        }
384
385        crate::info!(
386            release = RELEASE,
387            server_name = sentry.server_name.as_deref(),
388            environment = sentry.environment.as_deref(),
389            traces_sample_rate,
390            "Initialized Sentry client options"
391        );
392
393        let guard = sentry::init(options);
394
395        // Keep the client initialized. The client is flushed manually in `main`.
396        std::mem::forget(guard);
397    }
398
399    // Initialize native crash reporting after the Rust SDK, so that `capture_native_envelope` has
400    // access to an initialized Hub to capture crashes from the previous run.
401    #[cfg(feature = "crash-handler")]
402    {
403        if let Some(dsn) = sentry.enabled_dsn().map(|d| d.to_string())
404            && let Some(db) = sentry._crash_db.as_deref()
405        {
406            crate::info!("initializing crash handler in {}", db.display());
407            relay_crash::CrashHandler::new(dsn.as_str(), db)
408                .transport(capture_native_envelope)
409                .release(Some(RELEASE))
410                .environment(sentry.environment.as_deref())
411                .install();
412        }
413    }
414}