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