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