Skip to main content

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
17use crate::crash;
18
19/// The full release name including the Relay version and SHA.
20const RELEASE: &str = std::env!("RELAY_RELEASE");
21
22// Import CRATE_NAMES, which lists all crates in the workspace.
23include!(concat!(env!("OUT_DIR"), "/constants.gen.rs"));
24
25/// Controls the log format.
26#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)]
27#[serde(rename_all = "lowercase")]
28pub enum LogFormat {
29    /// Auto detect the best format.
30    ///
31    /// This chooses [`LogFormat::Pretty`] for TTY, otherwise [`LogFormat::Simplified`].
32    Auto,
33
34    /// Pretty printing with colors.
35    ///
36    /// ```text
37    ///  INFO  relay::setup > relay mode: managed
38    /// ```
39    Pretty,
40
41    /// Simplified plain text output.
42    ///
43    /// ```text
44    /// 2020-12-04T12:10:32Z [relay::setup] INFO: relay mode: managed
45    /// ```
46    Simplified,
47
48    /// Dump out JSON lines.
49    ///
50    /// ```text
51    /// {"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}
52    /// ```
53    Json,
54}
55
56/// The logging format parse error.
57#[derive(Clone, Debug)]
58pub struct FormatParseError(String);
59
60impl Display for FormatParseError {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        write!(
63            f,
64            r#"error parsing "{}" as format: expected one of "auto", "pretty", "simplified", "json""#,
65            self.0
66        )
67    }
68}
69
70impl FromStr for LogFormat {
71    type Err = FormatParseError;
72
73    fn from_str(s: &str) -> Result<Self, Self::Err> {
74        let result = match s {
75            "" => LogFormat::Auto,
76            s if s.eq_ignore_ascii_case("auto") => LogFormat::Auto,
77            s if s.eq_ignore_ascii_case("pretty") => LogFormat::Pretty,
78            s if s.eq_ignore_ascii_case("simplified") => LogFormat::Simplified,
79            s if s.eq_ignore_ascii_case("json") => LogFormat::Json,
80            s => return Err(FormatParseError(s.into())),
81        };
82
83        Ok(result)
84    }
85}
86
87impl std::error::Error for FormatParseError {}
88
89/// The logging level parse error.
90#[derive(Clone, Debug)]
91pub struct LevelParseError(String);
92
93impl Display for LevelParseError {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        write!(
96            f,
97            r#"error parsing "{}" as level: expected one of "error", "warn", "info", "debug", "trace", "off""#,
98            self.0
99        )
100    }
101}
102
103#[derive(Clone, Copy, Debug)]
104pub enum Level {
105    Error,
106    Warn,
107    Info,
108    Debug,
109    Trace,
110    Off,
111}
112
113impl_str_serde!(Level, "The logging level.");
114
115impl Level {
116    /// Returns the tracing [`LevelFilter`].
117    pub const fn level_filter(&self) -> LevelFilter {
118        match self {
119            Level::Error => LevelFilter::ERROR,
120            Level::Warn => LevelFilter::WARN,
121            Level::Info => LevelFilter::INFO,
122            Level::Debug => LevelFilter::DEBUG,
123            Level::Trace => LevelFilter::TRACE,
124            Level::Off => LevelFilter::OFF,
125        }
126    }
127}
128
129impl Display for Level {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        write!(f, "{}", format!("{self:?}").to_lowercase())
132    }
133}
134
135impl FromStr for Level {
136    type Err = LevelParseError;
137
138    fn from_str(s: &str) -> Result<Self, Self::Err> {
139        let result = match s {
140            "" => Level::Error,
141            s if s.eq_ignore_ascii_case("error") => Level::Error,
142            s if s.eq_ignore_ascii_case("warn") => Level::Warn,
143            s if s.eq_ignore_ascii_case("info") => Level::Info,
144            s if s.eq_ignore_ascii_case("debug") => Level::Debug,
145            s if s.eq_ignore_ascii_case("trace") => Level::Trace,
146            s if s.eq_ignore_ascii_case("off") => Level::Off,
147            s => return Err(LevelParseError(s.into())),
148        };
149
150        Ok(result)
151    }
152}
153
154impl std::error::Error for LevelParseError {}
155
156/// Controls the logging system.
157#[derive(Clone, Debug, Deserialize, Serialize)]
158#[serde(default)]
159pub struct LogConfig {
160    /// The log level for Relay.
161    pub level: Level,
162
163    /// Controls the log output format.
164    ///
165    /// Defaults to [`LogFormat::Auto`], which detects the best format based on the TTY.
166    pub format: LogFormat,
167
168    /// When set to `true`, backtraces are forced on.
169    ///
170    /// Otherwise, backtraces can be enabled by setting the `RUST_BACKTRACE` variable to `full`.
171    pub enable_backtraces: bool,
172
173    /// Sets the trace sample rate for performance monitoring.
174    ///
175    /// Defaults to `0.0` for release builds and `1.0` for local development builds.
176    pub traces_sample_rate: f32,
177}
178
179impl LogConfig {
180    /// Returns the tracing [`LevelFilter`].
181    pub const fn level_filter(&self) -> LevelFilter {
182        self.level.level_filter()
183    }
184}
185
186impl Default for LogConfig {
187    fn default() -> Self {
188        Self {
189            level: Level::Info,
190            format: LogFormat::Auto,
191            enable_backtraces: false,
192            #[cfg(debug_assertions)]
193            traces_sample_rate: 1.0,
194            #[cfg(not(debug_assertions))]
195            traces_sample_rate: 0.0,
196        }
197    }
198}
199
200/// Controls internal reporting to Sentry.
201#[derive(Clone, Debug, Deserialize, Serialize)]
202#[serde(default)]
203pub struct SentryConfig {
204    /// The [`DSN`](sentry::types::Dsn) specifying the Project to report to.
205    pub dsn: Option<Dsn>,
206
207    /// Enables reporting to Sentry.
208    pub enabled: bool,
209
210    /// Sets the environment for this service.
211    pub environment: Option<Cow<'static, str>>,
212
213    /// Sets the server name for this service.
214    ///
215    /// This is overridden by the `RELAY_SERVER_NAME`
216    /// environment variable.
217    pub server_name: Option<Cow<'static, str>>,
218
219    /// Add defaults tags to the events emitted by Relay
220    pub default_tags: Option<BTreeMap<String, String>>,
221
222    /// Internal. Enables crash handling and sets the absolute path to where minidumps should be
223    /// cached on disk. The path is created if it doesn't exist. Path must be UTF-8.
224    pub _crash_db: Option<PathBuf>,
225}
226
227impl SentryConfig {
228    /// Returns a reference to the [`DSN`](sentry::types::Dsn) if Sentry is enabled.
229    pub fn enabled_dsn(&self) -> Option<&Dsn> {
230        self.dsn.as_ref().filter(|_| self.enabled)
231    }
232}
233
234impl Default for SentryConfig {
235    fn default() -> Self {
236        Self {
237            dsn: "https://0cc4a37e5aab4da58366266a87a95740@sentry.io/1269704"
238                .parse()
239                .ok(),
240            enabled: false,
241            environment: None,
242            server_name: None,
243            default_tags: None,
244            _crash_db: None,
245        }
246    }
247}
248
249/// Configures the given log level for all of Relay's crates.
250fn get_default_filters() -> EnvFilter {
251    // Configure INFO as default, except for crates that are very spammy on INFO level.
252    let mut env_filter = EnvFilter::new(
253        "INFO,\
254        sqlx=WARN,\
255        tower_http=TRACE,\
256        trust_dns_proto=WARN,\
257        minidump=ERROR,\
258        metrics_exporter_dogstatsd::forwarder::sync=OFF,\
259        ",
260    );
261
262    // Add all internal modules with maximum log-level.
263    for name in CRATE_NAMES {
264        env_filter = env_filter.add_directive(format!("{name}=TRACE").parse().unwrap());
265    }
266
267    env_filter
268}
269
270/// Initialize the logging system and reporting to Sentry.
271///
272/// # Safety
273///
274/// The function is not safe to be called from a multi-threaded program,
275/// due to modifications of environment variables.
276///
277/// # Example
278///
279/// ```
280/// let log_config = relay_log::LogConfig {
281///     enable_backtraces: true,
282///     ..Default::default()
283/// };
284///
285/// let sentry_config = relay_log::SentryConfig::default();
286///
287/// unsafe { relay_log::init(&log_config, &sentry_config) };
288/// ```
289pub unsafe fn init(config: &LogConfig, sentry: &SentryConfig) {
290    if config.enable_backtraces {
291        unsafe {
292            env::set_var("RUST_BACKTRACE", "full");
293        }
294    }
295
296    let subscriber = tracing_subscriber::fmt::layer()
297        .with_writer(std::io::stderr)
298        .with_target(true);
299
300    let format = match (config.format, console::user_attended()) {
301        (LogFormat::Auto, true) | (LogFormat::Pretty, _) => {
302            subscriber.compact().without_time().boxed()
303        }
304        (LogFormat::Auto, false) | (LogFormat::Simplified, _) => {
305            subscriber.with_ansi(false).boxed()
306        }
307        (LogFormat::Json, _) => subscriber
308            .json()
309            .flatten_event(true)
310            .with_current_span(true)
311            .with_span_list(true)
312            .with_file(true)
313            .with_line_number(true)
314            .boxed(),
315    };
316
317    tracing_subscriber::registry()
318        .with(format.with_filter(config.level_filter()))
319        .with(
320            // Same as the default filter, except it converts warnings into events
321            // and also sends everything at or above INFO as logs instead of breadcrumbs.
322            sentry::integrations::tracing::layer().event_filter(|md| match *md.level() {
323                tracing::Level::ERROR | tracing::Level::WARN => {
324                    EventFilter::Event | EventFilter::Log
325                }
326                tracing::Level::INFO => EventFilter::Log,
327                tracing::Level::DEBUG | tracing::Level::TRACE => EventFilter::Ignore,
328            }),
329        )
330        .with(match env::var(EnvFilter::DEFAULT_ENV) {
331            Ok(value) => EnvFilter::new(value),
332            Err(_) => get_default_filters(),
333        })
334        .init();
335
336    if let Some(dsn) = sentry.enabled_dsn() {
337        let traces_sample_rate = config.traces_sample_rate;
338        // We're explicitly setting a `traces_sampler` here to circumvent trace
339        // propagation. A trace sampler that always just returns the constant
340        // `traces_sample_rate` is equivalent to using the `traces_sample_rate`
341        // directly, except it doesn't take into account whether the context
342        // was previously sampled. We don't want to take that into account because
343        // SDKs send headers with their envelopes that erroneously cause us to
344        // sample transactions.
345        let traces_sampler =
346            Some(Arc::new(move |_: &TransactionContext| traces_sample_rate) as Arc<TracesSampler>);
347        let mut options = sentry::ClientOptions {
348            dsn: Some(dsn).cloned(),
349            in_app_include: vec!["relay"],
350            release: Some(RELEASE.into()),
351            attach_stacktrace: config.enable_backtraces,
352            environment: sentry.environment.clone(),
353            server_name: sentry.server_name.clone(),
354            traces_sampler,
355            enable_logs: true,
356            ..Default::default()
357        };
358
359        // If `default_tags` is set in Sentry configuration install the `before_send` hook
360        // in order to inject said tags into each event
361        if let Some(default_tags) = sentry.default_tags.clone() {
362            // Install hook
363            options.before_send = Some(Arc::new(move |mut event| {
364                // Extend `event.tags` with `default_tags` without replacing tags already present
365                let previous_event_tags = std::mem::replace(&mut event.tags, default_tags.clone());
366                event.tags.extend(previous_event_tags);
367                Some(event)
368            }));
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        // Initialize native crash reporting after the Rust SDK, so that `capture_native_envelope` has
384        // access to an initialized Hub to capture crashes from the previous run.
385        #[cfg(feature = "crash-handler")]
386        {
387            crate::crash::init(sentry, (*guard).clone());
388        }
389
390        // Keep the client initialized. The client is flushed manually in `main`.
391        std::mem::forget(guard);
392    }
393}