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;
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
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/// Configures the given log level for all of Relay's crates.
248fn get_default_filters() -> EnvFilter {
249    // Configure INFO as default, except for crates that are very spammy on INFO level.
250    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    // Add all internal modules with maximum log-level.
261    for name in CRATE_NAMES {
262        env_filter = env_filter.add_directive(format!("{name}=TRACE").parse().unwrap());
263    }
264
265    env_filter
266}
267
268/// Initialize the logging system and reporting to Sentry.
269///
270/// # Safety
271///
272/// The function is not safe to be called from a multi-threaded program,
273/// due to modifications of environment variables.
274///
275/// # Example
276///
277/// ```
278/// let log_config = relay_log::LogConfig {
279///     enable_backtraces: true,
280///     ..Default::default()
281/// };
282///
283/// let sentry_config = relay_log::SentryConfig::default();
284///
285/// unsafe { relay_log::init(&log_config, &sentry_config) };
286/// ```
287pub 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            // Same as the default filter, except it converts warnings into events
319            // and also sends everything at or above INFO as logs instead of breadcrumbs.
320            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        // We're explicitly setting a `traces_sampler` here to circumvent trace
337        // propagation. A trace sampler that always just returns the constant
338        // `traces_sample_rate` is equivalent to using the `traces_sample_rate`
339        // directly, except it doesn't take into account whether the context
340        // was previously sampled. We don't want to take that into account because
341        // SDKs send headers with their envelopes that erroneously cause us to
342        // sample transactions.
343        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 `default_tags` is set in Sentry configuration install the `before_send` hook
353        // in order to inject said tags into each event
354        if let Some(default_tags) = sentry.default_tags.clone() {
355            // Install hook
356            options = options.before_send(move |mut event| {
357                // Extend `event.tags` with `default_tags` without replacing tags already present
358                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        // The crash reporter process runs inside `sentry::init` and never
365        // returns from it.
366        #[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        // Keep the client initialized. The client is flushed manually in `main`.
384        std::mem::forget(guard);
385    }
386}