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    /// Requires the `sentry` feature. When absent or when the DSN
35    /// is empty, Sentry is not initialised.
36    #[cfg(feature = "sentry")]
37    pub sentry: Option<super::sentry::SentryConfig>,
38}
39
40impl Default for Config {
41    fn default() -> Self {
42        Self {
43            level: "info".to_string(),
44            format: "pretty".to_string(),
45            #[cfg(feature = "sentry")]
46            sentry: None,
47        }
48    }
49}
50
51/// Initialise the global tracing subscriber.
52///
53/// Reads the log level from `RUST_LOG` if set; falls back to
54/// [`Config::level`] otherwise. Selects the output format from
55/// [`Config::format`].
56///
57/// When the `sentry` feature is enabled and a non-empty DSN is supplied,
58/// the Sentry SDK is also initialised and wired to the tracing subscriber
59/// via `sentry-tracing`.
60///
61/// Returns a [`TracingGuard`] that must be kept alive for the duration of
62/// the process. Dropping it flushes any buffered Sentry events.
63///
64/// Calling this function more than once in the same process is harmless —
65/// subsequent calls attempt `try_init` and silently ignore the
66/// "already initialised" error.
67///
68/// # Errors
69///
70/// Currently infallible. The `Result` return type is reserved for future
71/// validation of the [`Config`] fields at initialisation time.
72///
73/// [`TracingGuard`]: crate::tracing::TracingGuard
74pub fn init(config: &Config) -> crate::error::Result<super::sentry::TracingGuard> {
75    let filter =
76        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.level));
77
78    #[cfg(feature = "sentry")]
79    let sentry_guard = init_sentry(config);
80
81    match config.format.as_str() {
82        "json" => {
83            let base = tracing_subscriber::registry()
84                .with(filter)
85                .with(fmt::layer().json());
86            #[cfg(feature = "sentry")]
87            {
88                base.with(sentry_guard.as_ref().map(|_| sentry_tracing::layer()))
89                    .try_init()
90                    .ok();
91            }
92            #[cfg(not(feature = "sentry"))]
93            {
94                base.try_init().ok();
95            }
96        }
97        "pretty" => {
98            let base = tracing_subscriber::registry()
99                .with(filter)
100                .with(fmt::layer().pretty());
101            #[cfg(feature = "sentry")]
102            {
103                base.with(sentry_guard.as_ref().map(|_| sentry_tracing::layer()))
104                    .try_init()
105                    .ok();
106            }
107            #[cfg(not(feature = "sentry"))]
108            {
109                base.try_init().ok();
110            }
111        }
112        _ => {
113            let base = tracing_subscriber::registry()
114                .with(filter)
115                .with(fmt::layer());
116            #[cfg(feature = "sentry")]
117            {
118                base.with(sentry_guard.as_ref().map(|_| sentry_tracing::layer()))
119                    .try_init()
120                    .ok();
121            }
122            #[cfg(not(feature = "sentry"))]
123            {
124                base.try_init().ok();
125            }
126        }
127    }
128
129    #[cfg(feature = "sentry")]
130    {
131        Ok(match sentry_guard {
132            Some(g) => super::sentry::TracingGuard::with_sentry(g),
133            None => super::sentry::TracingGuard::new(),
134        })
135    }
136    #[cfg(not(feature = "sentry"))]
137    {
138        Ok(super::sentry::TracingGuard::new())
139    }
140}
141
142#[cfg(feature = "sentry")]
143fn init_sentry(config: &Config) -> Option<sentry::ClientInitGuard> {
144    config
145        .sentry
146        .as_ref()
147        .filter(|sc| !sc.dsn.is_empty())
148        .map(|sentry_config| {
149            sentry::init((
150                sentry_config.dsn.as_str(),
151                sentry::ClientOptions {
152                    release: sentry::release_name!(),
153                    environment: Some(sentry_config.environment.clone().into()),
154                    sample_rate: sentry_config.sample_rate,
155                    traces_sample_rate: sentry_config.traces_sample_rate,
156                    ..Default::default()
157                },
158            ))
159        })
160}