1#![warn(missing_docs)]
21
22pub mod context;
24pub mod error;
26pub mod fields;
28pub mod masking;
30pub mod module_levels;
32#[cfg(feature = "otlp")]
34pub mod otlp;
35pub mod sampling;
37
38use std::collections::HashMap;
39use std::fs::OpenOptions;
40use std::sync::Arc;
41
42use rskit_config::{LogFormat, LogOutput, LoggingConfig};
43use tracing::dispatcher::DefaultGuard;
44use tracing_subscriber::{EnvFilter, fmt, fmt::writer::BoxMakeWriter, layer::SubscriberExt};
45
46pub use error::LoggingResult;
47pub use masking::{DefaultMasker, Masker, MaskingConfig, MaskingMakeWriter};
48pub use module_levels::{ModuleLevelsConfig, build_env_filter};
49#[cfg(feature = "otlp")]
50pub use otlp::{OtlpConfig, OtlpProvider};
51pub use sampling::SamplingConfig;
52
53#[cfg(feature = "otlp")]
55pub struct LoggingSetup<'a> {
56 pub config: &'a LoggingConfig,
58 pub sampling: Option<&'a SamplingConfig>,
60 pub module_levels: Option<&'a HashMap<String, String>>,
62 pub masking: Option<&'a MaskingConfig>,
64 pub otlp: Option<&'a otlp::OtlpConfig>,
66 pub service_name: &'a str,
68 pub environment: &'a str,
70 pub version: &'a str,
72}
73
74#[cfg(feature = "otlp")]
75impl<'a> LoggingSetup<'a> {
76 #[must_use]
78 pub const fn new(
79 config: &'a LoggingConfig,
80 service_name: &'a str,
81 environment: &'a str,
82 version: &'a str,
83 ) -> Self {
84 Self {
85 config,
86 sampling: None,
87 module_levels: None,
88 masking: None,
89 otlp: None,
90 service_name,
91 environment,
92 version,
93 }
94 }
95
96 #[must_use]
98 pub const fn with_sampling(mut self, sampling: &'a SamplingConfig) -> Self {
99 self.sampling = Some(sampling);
100 self
101 }
102
103 #[must_use]
105 pub const fn with_module_levels(mut self, module_levels: &'a HashMap<String, String>) -> Self {
106 self.module_levels = Some(module_levels);
107 self
108 }
109
110 #[must_use]
112 pub const fn with_masking(mut self, masking: &'a MaskingConfig) -> Self {
113 self.masking = Some(masking);
114 self
115 }
116
117 #[must_use]
119 pub const fn with_otlp(mut self, otlp: &'a otlp::OtlpConfig) -> Self {
120 self.otlp = Some(otlp);
121 self
122 }
123}
124
125pub struct LoggingGuard {
132 #[allow(dead_code)]
133 guard: DefaultGuard,
134 #[cfg(feature = "otlp")]
135 otlp_provider: Option<otlp::OtlpProvider>,
136}
137
138impl LoggingGuard {
139 fn new(guard: DefaultGuard) -> Self {
140 Self {
141 guard,
142 #[cfg(feature = "otlp")]
143 otlp_provider: None,
144 }
145 }
146
147 #[cfg(feature = "otlp")]
148 fn with_otlp_provider(guard: DefaultGuard, otlp_provider: Option<otlp::OtlpProvider>) -> Self {
149 Self {
150 guard,
151 otlp_provider,
152 }
153 }
154}
155
156#[cfg(feature = "otlp")]
157impl Drop for LoggingGuard {
158 fn drop(&mut self) {
159 if let Some(provider) = self.otlp_provider.take()
160 && let Err(error) = provider.shutdown()
161 {
162 tracing::warn!(%error, "failed to shut down OTLP logging provider");
163 }
164 }
165}
166
167pub fn init_logging(cfg: &LoggingConfig) -> LoggingResult<LoggingGuard> {
182 init_logging_with_default_masking(cfg)
183}
184
185pub fn init_logging_env() -> LoggingGuard {
187 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
188 let layer = fmt::layer().pretty();
189 let dispatcher = tracing_subscriber::registry()
190 .with(filter)
191 .with(layer)
192 .into();
193 LoggingGuard::new(tracing::dispatcher::set_default(&dispatcher))
194}
195
196fn init_logging_with_default_masking(cfg: &LoggingConfig) -> LoggingResult<LoggingGuard> {
197 let filter = build_filter(&cfg.level, None);
198 let masker: Arc<dyn masking::Masker> = Arc::new(masking::DefaultMasker::default());
199 let writer = masking::MaskingMakeWriter::new(build_output_writer(&cfg.output)?, masker);
200
201 let guard = match cfg.format {
202 LogFormat::Json => {
203 let layer = fmt::layer()
204 .json()
205 .with_current_span(true)
206 .with_span_list(true)
207 .with_writer(writer);
208 let dispatcher = tracing_subscriber::registry()
209 .with(filter)
210 .with(layer)
211 .into();
212 tracing::dispatcher::set_default(&dispatcher)
213 }
214 _ => {
215 let layer = fmt::layer().pretty().with_writer(writer);
216 let dispatcher = tracing_subscriber::registry()
217 .with(filter)
218 .with(layer)
219 .into();
220 tracing::dispatcher::set_default(&dispatcher)
221 }
222 };
223
224 Ok(LoggingGuard::new(guard))
225}
226
227pub fn init_logging_with_masking(
234 cfg: &LoggingConfig,
235 masking_cfg: &masking::MaskingConfig,
236) -> LoggingResult<LoggingGuard> {
237 let m = if masking_cfg.enabled {
238 Some(masking_cfg)
239 } else {
240 None
241 };
242 init_logging_with_options(cfg, None, None, m)
243}
244
245pub fn init_logging_with_options(
264 cfg: &LoggingConfig,
265 sampling_cfg: Option<&SamplingConfig>,
266 module_levels: Option<&HashMap<String, String>>,
267 masking_cfg: Option<&MaskingConfig>,
268) -> LoggingResult<LoggingGuard> {
269 let filter = build_filter(&cfg.level, module_levels);
270
271 let sampling_layer = sampling_cfg
272 .filter(|s| s.enabled)
273 .map(sampling::SamplingLayer::new);
274 let writer = build_output_writer(&cfg.output)?;
275
276 if let Some(m) = masking_cfg.filter(|m| m.enabled) {
277 let masker: Arc<dyn masking::Masker> = Arc::new(masking::DefaultMasker::new(m)?);
278 let writer = masking::MaskingMakeWriter::new(writer, masker);
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 return Ok(LoggingGuard::new(guard));
305 }
306
307 let guard = match cfg.format {
308 LogFormat::Json => {
309 let layer = fmt::layer()
310 .json()
311 .with_current_span(true)
312 .with_span_list(true)
313 .with_writer(writer);
314 let dispatcher = tracing_subscriber::registry()
315 .with(filter)
316 .with(sampling_layer)
317 .with(layer)
318 .into();
319 tracing::dispatcher::set_default(&dispatcher)
320 }
321 _ => {
322 let layer = fmt::layer().pretty().with_writer(writer);
323 let dispatcher = tracing_subscriber::registry()
324 .with(filter)
325 .with(sampling_layer)
326 .with(layer)
327 .into();
328 tracing::dispatcher::set_default(&dispatcher)
329 }
330 };
331
332 Ok(LoggingGuard::new(guard))
333}
334
335fn build_filter(level: &str, module_levels: Option<&HashMap<String, String>>) -> EnvFilter {
337 match module_levels {
338 Some(levels) if !levels.is_empty() => module_levels::build_env_filter(level, levels),
339 _ => EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level)),
340 }
341}
342
343fn build_output_writer(output: &LogOutput) -> LoggingResult<BoxMakeWriter> {
344 let writer = match output {
345 LogOutput::Stdout => BoxMakeWriter::new(std::io::stdout),
346 LogOutput::Stderr => BoxMakeWriter::new(std::io::stderr),
347 LogOutput::File { path } => {
348 let file = OpenOptions::new()
349 .create(true)
350 .append(true)
351 .open(path)
352 .map_err(|err| error::log_file_open(path.clone(), err))?;
353 BoxMakeWriter::new(Arc::new(file))
354 }
355 _ => return Err(error::unsupported_output()),
356 };
357
358 Ok(writer)
359}
360
361#[cfg(feature = "otlp")]
380pub fn init_logging_full(setup: LoggingSetup<'_>) -> LoggingResult<LoggingGuard> {
381 let filter = build_filter(&setup.config.level, setup.module_levels);
382
383 let sampling_layer = setup
384 .sampling
385 .filter(|s| s.enabled)
386 .map(sampling::SamplingLayer::new);
387 let writer = build_output_writer(&setup.config.output)?;
388
389 let otlp_provider = match setup.otlp {
390 Some(oc) => {
391 otlp::OtlpProvider::new(oc, setup.service_name, setup.environment, setup.version)?
392 }
393 None => None,
394 };
395 let otlp_layer = otlp_provider
396 .as_ref()
397 .map(|p| p.layer::<tracing_subscriber::Registry>());
398
399 if let Some(m) = setup.masking.filter(|m| m.enabled) {
400 let masker: Arc<dyn masking::Masker> = Arc::new(masking::DefaultMasker::new(m)?);
401 let writer = masking::MaskingMakeWriter::new(writer, masker);
402
403 let guard = match setup.config.format {
404 LogFormat::Json => {
405 let layer = fmt::layer()
406 .json()
407 .with_current_span(true)
408 .with_span_list(true)
409 .with_writer(writer);
410 let dispatcher = tracing_subscriber::registry()
411 .with(filter)
412 .with(sampling_layer)
413 .with(layer)
414 .with(otlp_layer)
415 .into();
416 tracing::dispatcher::set_default(&dispatcher)
417 }
418 _ => {
419 let layer = fmt::layer().pretty().with_writer(writer);
420 let dispatcher = tracing_subscriber::registry()
421 .with(filter)
422 .with(sampling_layer)
423 .with(layer)
424 .with(otlp_layer)
425 .into();
426 tracing::dispatcher::set_default(&dispatcher)
427 }
428 };
429 return Ok(LoggingGuard::with_otlp_provider(guard, otlp_provider));
430 }
431
432 let guard = match setup.config.format {
433 LogFormat::Json => {
434 let layer = fmt::layer()
435 .json()
436 .with_current_span(true)
437 .with_span_list(true)
438 .with_writer(writer);
439 let dispatcher = tracing_subscriber::registry()
440 .with(filter)
441 .with(sampling_layer)
442 .with(layer)
443 .with(otlp_layer)
444 .into();
445 tracing::dispatcher::set_default(&dispatcher)
446 }
447 _ => {
448 let layer = fmt::layer().pretty().with_writer(writer);
449 let dispatcher = tracing_subscriber::registry()
450 .with(filter)
451 .with(sampling_layer)
452 .with(layer)
453 .with(otlp_layer)
454 .into();
455 tracing::dispatcher::set_default(&dispatcher)
456 }
457 };
458
459 Ok(LoggingGuard::with_otlp_provider(guard, otlp_provider))
460}
461
462pub use tracing::{debug, error, info, instrument, trace, warn};
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use rskit_config::LoggingConfig;
469
470 #[test]
471 fn init_console_does_not_panic() {
472 let cfg = LoggingConfig::default();
473 let _guard = init_logging(&cfg).unwrap();
474 tracing::info!("test log");
475 }
476
477 #[test]
478 fn init_json_does_not_panic() {
479 let cfg = LoggingConfig {
480 format: rskit_config::LogFormat::Json,
481 ..Default::default()
482 };
483 let _guard = init_logging(&cfg).unwrap();
484 tracing::info!("test json log");
485 }
486}