1use std::collections::HashMap;
9use std::fs::OpenOptions;
10use std::sync::Arc;
11
12use tracing::dispatcher::DefaultGuard;
13use tracing_subscriber::{EnvFilter, fmt, fmt::writer::BoxMakeWriter, layer::SubscriberExt};
14
15use crate::config::{LogFormat, LogOutput, LoggingConfig};
16use crate::error::{self, LoggingResult};
17use crate::masking::{self, MaskingConfig};
18use crate::module_levels;
19use crate::sampling::{self, SamplingConfig};
20
21#[cfg(feature = "otlp")]
23pub struct LoggingSetup<'a> {
24 pub config: &'a LoggingConfig,
26 pub sampling: Option<&'a SamplingConfig>,
28 pub module_levels: Option<&'a HashMap<String, String>>,
30 pub masking: Option<&'a MaskingConfig>,
32 pub otlp: Option<&'a crate::otlp::OtlpConfig>,
34 pub service_name: &'a str,
36 pub environment: &'a str,
38 pub version: &'a str,
40}
41
42#[cfg(feature = "otlp")]
43impl<'a> LoggingSetup<'a> {
44 #[must_use]
46 pub const fn new(
47 config: &'a LoggingConfig,
48 service_name: &'a str,
49 environment: &'a str,
50 version: &'a str,
51 ) -> Self {
52 Self {
53 config,
54 sampling: None,
55 module_levels: None,
56 masking: None,
57 otlp: None,
58 service_name,
59 environment,
60 version,
61 }
62 }
63
64 #[must_use]
66 pub const fn with_sampling(mut self, sampling: &'a SamplingConfig) -> Self {
67 self.sampling = Some(sampling);
68 self
69 }
70
71 #[must_use]
73 pub const fn with_module_levels(mut self, module_levels: &'a HashMap<String, String>) -> Self {
74 self.module_levels = Some(module_levels);
75 self
76 }
77
78 #[must_use]
80 pub const fn with_masking(mut self, masking: &'a MaskingConfig) -> Self {
81 self.masking = Some(masking);
82 self
83 }
84
85 #[must_use]
87 pub const fn with_otlp(mut self, otlp: &'a crate::otlp::OtlpConfig) -> Self {
88 self.otlp = Some(otlp);
89 self
90 }
91}
92
93pub struct LoggingGuard {
99 #[allow(dead_code)]
100 guard: DefaultGuard,
101 #[cfg(feature = "otlp")]
102 otlp_provider: Option<crate::otlp::OtlpProvider>,
103}
104
105impl LoggingGuard {
106 fn new(guard: DefaultGuard) -> Self {
107 Self {
108 guard,
109 #[cfg(feature = "otlp")]
110 otlp_provider: None,
111 }
112 }
113
114 #[cfg(feature = "otlp")]
115 fn with_otlp_provider(
116 guard: DefaultGuard,
117 otlp_provider: Option<crate::otlp::OtlpProvider>,
118 ) -> Self {
119 Self {
120 guard,
121 otlp_provider,
122 }
123 }
124}
125
126#[cfg(feature = "otlp")]
127impl Drop for LoggingGuard {
128 fn drop(&mut self) {
129 if let Some(provider) = self.otlp_provider.take()
130 && let Err(error) = provider.shutdown()
131 {
132 tracing::warn!(%error, "failed to shut down OTLP logging provider");
133 }
134 }
135}
136
137pub fn init_logging(cfg: &LoggingConfig) -> LoggingResult<LoggingGuard> {
151 init_logging_with_default_masking(cfg)
152}
153
154pub fn init_logging_env() -> LoggingGuard {
156 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
157 let layer = fmt::layer().pretty();
158 let dispatcher = tracing_subscriber::registry()
159 .with(filter)
160 .with(layer)
161 .into();
162 LoggingGuard::new(tracing::dispatcher::set_default(&dispatcher))
163}
164
165fn init_logging_with_default_masking(cfg: &LoggingConfig) -> LoggingResult<LoggingGuard> {
166 let filter = build_filter(&cfg.level, None);
167 let masker: Arc<dyn masking::Masker> = Arc::new(masking::DefaultMasker::default());
168 let writer = masking::MaskingMakeWriter::new(build_output_writer(&cfg.output)?, masker);
169
170 let guard = match cfg.format {
171 LogFormat::Json => {
172 let layer = fmt::layer()
173 .json()
174 .with_current_span(true)
175 .with_span_list(true)
176 .with_writer(writer);
177 let dispatcher = tracing_subscriber::registry()
178 .with(filter)
179 .with(layer)
180 .into();
181 tracing::dispatcher::set_default(&dispatcher)
182 }
183 _ => {
184 let layer = fmt::layer().pretty().with_writer(writer);
185 let dispatcher = tracing_subscriber::registry()
186 .with(filter)
187 .with(layer)
188 .into();
189 tracing::dispatcher::set_default(&dispatcher)
190 }
191 };
192
193 Ok(LoggingGuard::new(guard))
194}
195
196pub fn init_logging_with_masking(
203 cfg: &LoggingConfig,
204 masking_cfg: &masking::MaskingConfig,
205) -> LoggingResult<LoggingGuard> {
206 let m = if masking_cfg.enabled {
207 Some(masking_cfg)
208 } else {
209 None
210 };
211 init_logging_with_options(cfg, None, None, m)
212}
213
214pub fn init_logging_with_options(
231 cfg: &LoggingConfig,
232 sampling_cfg: Option<&SamplingConfig>,
233 module_levels: Option<&HashMap<String, String>>,
234 masking_cfg: Option<&MaskingConfig>,
235) -> LoggingResult<LoggingGuard> {
236 let filter = build_filter(&cfg.level, module_levels);
237
238 let sampling_layer = sampling_cfg
239 .filter(|s| s.enabled)
240 .map(sampling::SamplingLayer::new);
241 let writer = build_output_writer(&cfg.output)?;
242
243 if let Some(m) = masking_cfg.filter(|m| m.enabled) {
244 let masker: Arc<dyn masking::Masker> = Arc::new(masking::DefaultMasker::new(m)?);
245 let writer = masking::MaskingMakeWriter::new(writer, masker);
246
247 let guard = match cfg.format {
248 LogFormat::Json => {
249 let layer = fmt::layer()
250 .json()
251 .with_current_span(true)
252 .with_span_list(true)
253 .with_writer(writer);
254 let dispatcher = tracing_subscriber::registry()
255 .with(filter)
256 .with(sampling_layer)
257 .with(layer)
258 .into();
259 tracing::dispatcher::set_default(&dispatcher)
260 }
261 _ => {
262 let layer = fmt::layer().pretty().with_writer(writer);
263 let dispatcher = tracing_subscriber::registry()
264 .with(filter)
265 .with(sampling_layer)
266 .with(layer)
267 .into();
268 tracing::dispatcher::set_default(&dispatcher)
269 }
270 };
271 return Ok(LoggingGuard::new(guard));
272 }
273
274 let guard = match cfg.format {
275 LogFormat::Json => {
276 let layer = fmt::layer()
277 .json()
278 .with_current_span(true)
279 .with_span_list(true)
280 .with_writer(writer);
281 let dispatcher = tracing_subscriber::registry()
282 .with(filter)
283 .with(sampling_layer)
284 .with(layer)
285 .into();
286 tracing::dispatcher::set_default(&dispatcher)
287 }
288 _ => {
289 let layer = fmt::layer().pretty().with_writer(writer);
290 let dispatcher = tracing_subscriber::registry()
291 .with(filter)
292 .with(sampling_layer)
293 .with(layer)
294 .into();
295 tracing::dispatcher::set_default(&dispatcher)
296 }
297 };
298
299 Ok(LoggingGuard::new(guard))
300}
301
302fn build_filter(level: &str, module_levels: Option<&HashMap<String, String>>) -> EnvFilter {
304 match module_levels {
305 Some(levels) if !levels.is_empty() => module_levels::build_env_filter(level, levels),
306 _ => EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level)),
307 }
308}
309
310fn build_output_writer(output: &LogOutput) -> LoggingResult<BoxMakeWriter> {
311 let writer = match output {
312 LogOutput::Stdout => BoxMakeWriter::new(std::io::stdout),
313 LogOutput::Stderr => BoxMakeWriter::new(std::io::stderr),
314 LogOutput::File { path } => {
315 let file = OpenOptions::new()
316 .create(true)
317 .append(true)
318 .open(path)
319 .map_err(|err| error::log_file_open(path.clone(), err))?;
320 BoxMakeWriter::new(Arc::new(file))
321 }
322 };
323
324 Ok(writer)
325}
326
327#[cfg(feature = "otlp")]
345pub fn init_logging_full(setup: LoggingSetup<'_>) -> LoggingResult<LoggingGuard> {
346 use crate::otlp;
347
348 let filter = build_filter(&setup.config.level, setup.module_levels);
349
350 let sampling_layer = setup
351 .sampling
352 .filter(|s| s.enabled)
353 .map(sampling::SamplingLayer::new);
354 let writer = build_output_writer(&setup.config.output)?;
355
356 let otlp_provider = match setup.otlp {
357 Some(oc) => {
358 otlp::OtlpProvider::new(oc, setup.service_name, setup.environment, setup.version)?
359 }
360 None => None,
361 };
362 let otlp_layer = otlp_provider
363 .as_ref()
364 .map(|p| p.layer::<tracing_subscriber::Registry>());
365
366 if let Some(m) = setup.masking.filter(|m| m.enabled) {
367 let masker: Arc<dyn masking::Masker> = Arc::new(masking::DefaultMasker::new(m)?);
368 let writer = masking::MaskingMakeWriter::new(writer, masker);
369
370 let guard = match setup.config.format {
371 LogFormat::Json => {
372 let layer = fmt::layer()
373 .json()
374 .with_current_span(true)
375 .with_span_list(true)
376 .with_writer(writer);
377 let dispatcher = tracing_subscriber::registry()
378 .with(filter)
379 .with(sampling_layer)
380 .with(layer)
381 .with(otlp_layer)
382 .into();
383 tracing::dispatcher::set_default(&dispatcher)
384 }
385 _ => {
386 let layer = fmt::layer().pretty().with_writer(writer);
387 let dispatcher = tracing_subscriber::registry()
388 .with(filter)
389 .with(sampling_layer)
390 .with(layer)
391 .with(otlp_layer)
392 .into();
393 tracing::dispatcher::set_default(&dispatcher)
394 }
395 };
396 return Ok(LoggingGuard::with_otlp_provider(guard, otlp_provider));
397 }
398
399 let guard = match setup.config.format {
400 LogFormat::Json => {
401 let layer = fmt::layer()
402 .json()
403 .with_current_span(true)
404 .with_span_list(true)
405 .with_writer(writer);
406 let dispatcher = tracing_subscriber::registry()
407 .with(filter)
408 .with(sampling_layer)
409 .with(layer)
410 .with(otlp_layer)
411 .into();
412 tracing::dispatcher::set_default(&dispatcher)
413 }
414 _ => {
415 let layer = fmt::layer().pretty().with_writer(writer);
416 let dispatcher = tracing_subscriber::registry()
417 .with(filter)
418 .with(sampling_layer)
419 .with(layer)
420 .with(otlp_layer)
421 .into();
422 tracing::dispatcher::set_default(&dispatcher)
423 }
424 };
425
426 Ok(LoggingGuard::with_otlp_provider(guard, otlp_provider))
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn init_console_does_not_panic() {
435 let cfg = LoggingConfig::default();
436 let _guard = init_logging(&cfg).unwrap();
437 tracing::info!("test log");
438 }
439
440 #[test]
441 fn init_json_does_not_panic() {
442 let cfg = LoggingConfig {
443 format: LogFormat::Json,
444 ..Default::default()
445 };
446 let _guard = init_logging(&cfg).unwrap();
447 tracing::info!("test json log");
448 }
449}