Skip to main content

modo/tracing/
init.rs

1use serde::Deserialize;
2use tracing_subscriber::prelude::*;
3use tracing_subscriber::{EnvFilter, fmt};
4
5/// Configuration for the tracing subscriber.
6///
7/// Embedded in the top-level `modo::Config` as the `tracing` section:
8///
9/// ```yaml
10/// tracing:
11///   level: info
12///   format: pretty   # "pretty" | "json" | compact (any other value)
13/// ```
14///
15/// All fields have sane defaults so the entire section can be omitted.
16#[non_exhaustive]
17#[derive(Debug, Clone, Deserialize)]
18#[serde(default)]
19pub struct Config {
20    /// Minimum log level when `RUST_LOG` is not set.
21    ///
22    /// Accepts any valid [`tracing_subscriber::EnvFilter`] directive such as
23    /// `"info"`, `"debug"`, or `"myapp=debug,modo=info"`.
24    /// Defaults to `"info"`.
25    pub level: String,
26
27    /// Output format: `"pretty"`, `"json"`, or compact (any other value).
28    ///
29    /// Defaults to `"pretty"`.
30    pub format: String,
31
32    /// Sentry error-reporting settings.
33    ///
34    /// When absent or when the DSN is empty, Sentry is not initialised.
35    pub sentry: Option<super::sentry::SentryConfig>,
36}
37
38impl Default for Config {
39    fn default() -> Self {
40        Self {
41            level: "info".to_string(),
42            format: "pretty".to_string(),
43            sentry: None,
44        }
45    }
46}
47
48/// Initialise the global tracing subscriber.
49///
50/// Reads the log level from `RUST_LOG` if set; falls back to
51/// [`Config::level`] otherwise. Selects the output format from
52/// [`Config::format`].
53///
54/// When [`Config::sentry`] contains a non-empty DSN, the Sentry SDK is
55/// also initialised and wired to the tracing subscriber via
56/// `sentry-tracing`. Sentry support is always compiled in — no feature
57/// flag is required.
58///
59/// Returns a [`TracingGuard`] that must be kept alive for the duration of
60/// the process. Dropping it flushes any buffered Sentry events.
61///
62/// Calling this function more than once in the same process is harmless —
63/// subsequent calls attempt `try_init` and silently ignore the
64/// "already initialised" error.
65///
66/// # Errors
67///
68/// Currently infallible. The `Result` return type is reserved for future
69/// validation of the [`Config`] fields at initialisation time.
70///
71/// [`TracingGuard`]: crate::tracing::TracingGuard
72pub fn init(config: &Config) -> crate::error::Result<super::sentry::TracingGuard> {
73    let filter =
74        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.level));
75
76    let sentry_guard = init_sentry(config);
77
78    match config.format.as_str() {
79        "json" => {
80            let base = tracing_subscriber::registry()
81                .with(filter)
82                .with(fmt::layer().json());
83            base.with(sentry_guard.as_ref().map(|_| sentry_tracing::layer()))
84                .try_init()
85                .ok();
86        }
87        "pretty" => {
88            let base = tracing_subscriber::registry()
89                .with(filter)
90                .with(fmt::layer().pretty());
91            base.with(sentry_guard.as_ref().map(|_| sentry_tracing::layer()))
92                .try_init()
93                .ok();
94        }
95        _ => {
96            let base = tracing_subscriber::registry()
97                .with(filter)
98                .with(fmt::layer());
99            base.with(sentry_guard.as_ref().map(|_| sentry_tracing::layer()))
100                .try_init()
101                .ok();
102        }
103    }
104
105    Ok(match sentry_guard {
106        Some(g) => super::sentry::TracingGuard::with_sentry(g),
107        None => super::sentry::TracingGuard::new(),
108    })
109}
110
111fn init_sentry(config: &Config) -> Option<sentry::ClientInitGuard> {
112    config
113        .sentry
114        .as_ref()
115        .filter(|sc| !sc.dsn.is_empty())
116        .map(|sentry_config| {
117            sentry::init((
118                sentry_config.dsn.as_str(),
119                sentry::ClientOptions {
120                    release: sentry::release_name!(),
121                    environment: Some(sentry_config.environment.clone().into()),
122                    sample_rate: sentry_config.sample_rate,
123                    traces_sample_rate: sentry_config.traces_sample_rate,
124                    ..Default::default()
125                },
126            ))
127        })
128}