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}