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}