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        ",
273    );
274
275    // Add all internal modules with maximum log-level.
276    for name in CRATE_NAMES {
277        env_filter = env_filter.add_directive(format!("{name}=TRACE").parse().unwrap());
278    }
279
280    env_filter
281}
282
283/// Initialize the logging system and reporting to Sentry.
284///
285/// # Safety
286///
287/// The function is not safe to be called from a multi-threaded program,
288/// due to modifications of environment variables.
289///
290/// # Example
291///
292/// ```
293/// let log_config = relay_log::LogConfig {
294///     enable_backtraces: true,
295///     ..Default::default()
296/// };
297///
298/// let sentry_config = relay_log::SentryConfig::default();
299///
300/// unsafe { relay_log::init(&log_config, &sentry_config) };
301/// ```
302pub 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            // Same as the default filter, except it converts warnings into events instead of breadcrumbs.
334            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        // We're explicitly setting a `traces_sampler` here to circumvent trace
350        // propagation. A trace sampler that always just returns the constant
351        // `traces_sample_rate` is equivalent to using the `traces_sample_rate`
352        // directly, except it doesn't take into account whether the context
353        // was previously sampled. We don't want to take that into account because
354        // SDKs send headers with their envelopes that erroneously cause us to
355        // sample transactions.
356        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 `default_tags` is set in Sentry configuration install the `before_send` hook
370        // in order to inject said tags into each event
371        if let Some(default_tags) = sentry.default_tags.clone() {
372            // Install hook
373            options.before_send = Some(Arc::new(move |mut event| {
374                // Extend `event.tags` with `default_tags` without replacing tags already present
375                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        // Keep the client initialized. The client is flushed manually in `main`.
392        std::mem::forget(guard);
393    }
394
395    // Initialize native crash reporting after the Rust SDK, so that `capture_native_envelope` has
396    // access to an initialized Hub to capture crashes from the previous run.
397    #[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}