1use std::path::Path;
7use std::sync::OnceLock;
8
9use tracing_appender::rolling::{RollingFileAppender, Rotation};
10use tracing_subscriber::{
11 fmt::{self, format::FmtSpan, time::UtcTime},
12 layer::SubscriberExt,
13 reload,
14 util::SubscriberInitExt,
15 EnvFilter,
16};
17
18use super::config::{LogConfig, LogFormat, LogTarget};
19use super::dynamic::LogLevelController;
20use super::rotation::RotationStrategy;
21use crate::error::Result;
22
23static LOGGING_INITIALIZED: OnceLock<()> = OnceLock::new();
25
26pub fn init_logging(config: &LogConfig) -> Result<()> {
46 if LOGGING_INITIALIZED.get().is_some() {
48 return Ok(());
49 }
50
51 config.validate()?;
53
54 let result = init_logging_internal(config);
55
56 if result.is_ok() {
57 LOGGING_INITIALIZED.get_or_init(|| ());
58 }
59
60 result
61}
62
63fn init_logging_internal(config: &LogConfig) -> Result<()> {
65 let filter = build_env_filter(config);
66 let span_events = build_span_events(config);
67
68 if config.dynamic_level {
70 init_with_dynamic_level(config, filter, span_events)
71 } else {
72 init_static(config, filter, span_events)
73 }
74}
75
76fn init_with_dynamic_level(
78 config: &LogConfig,
79 filter: EnvFilter,
80 span_events: FmtSpan,
81) -> Result<()> {
82 let (filter_layer, reload_handle) = reload::Layer::new(filter);
83
84 match &config.target {
85 LogTarget::Stdout => {
86 init_stdout_with_reload(config, filter_layer, span_events)?;
87 }
88 LogTarget::Stderr => {
89 init_stderr_with_reload(config, filter_layer, span_events)?;
90 }
91 LogTarget::File {
92 directory,
93 filename_prefix,
94 rotation,
95 } => {
96 init_file_with_reload(
97 config,
98 filter_layer,
99 span_events,
100 directory,
101 filename_prefix,
102 rotation,
103 )?;
104 }
105 LogTarget::Multi(targets) => {
106 init_multi_with_reload(config, filter_layer, span_events, targets)?;
109 }
110 }
111
112 let controller = LogLevelController::new(reload_handle, config.level);
114 controller.register_global();
115
116 Ok(())
117}
118
119fn init_stdout_with_reload(
121 config: &LogConfig,
122 filter_layer: reload::Layer<EnvFilter, tracing_subscriber::Registry>,
123 span_events: FmtSpan,
124) -> Result<()> {
125 let registry = tracing_subscriber::registry().with(filter_layer);
126
127 match config.format {
128 LogFormat::Json => {
129 let layer = fmt::layer()
130 .json()
131 .with_timer(UtcTime::rfc_3339())
132 .with_file(config.include_location)
133 .with_line_number(config.include_location)
134 .with_target(config.include_target)
135 .with_thread_ids(config.include_thread_ids)
136 .with_thread_names(config.include_thread_names)
137 .with_span_events(span_events)
138 .with_writer(std::io::stdout);
139 registry.with(layer).init();
140 }
141 LogFormat::Pretty => {
142 let layer = fmt::layer()
143 .pretty()
144 .with_timer(UtcTime::rfc_3339())
145 .with_file(config.include_location)
146 .with_line_number(config.include_location)
147 .with_target(config.include_target)
148 .with_thread_ids(config.include_thread_ids)
149 .with_thread_names(config.include_thread_names)
150 .with_span_events(span_events)
151 .with_ansi(config.ansi_colors)
152 .with_writer(std::io::stdout);
153 registry.with(layer).init();
154 }
155 LogFormat::Compact => {
156 let layer = fmt::layer()
157 .compact()
158 .with_timer(UtcTime::rfc_3339())
159 .with_file(config.include_location)
160 .with_line_number(config.include_location)
161 .with_target(config.include_target)
162 .with_thread_ids(config.include_thread_ids)
163 .with_thread_names(config.include_thread_names)
164 .with_span_events(span_events)
165 .with_ansi(config.ansi_colors)
166 .with_writer(std::io::stdout);
167 registry.with(layer).init();
168 }
169 LogFormat::Full => {
170 let layer = fmt::layer()
171 .with_timer(UtcTime::rfc_3339())
172 .with_file(config.include_location)
173 .with_line_number(config.include_location)
174 .with_target(config.include_target)
175 .with_thread_ids(config.include_thread_ids)
176 .with_thread_names(config.include_thread_names)
177 .with_span_events(span_events)
178 .with_ansi(config.ansi_colors)
179 .with_writer(std::io::stdout);
180 registry.with(layer).init();
181 }
182 }
183
184 Ok(())
185}
186
187fn init_stderr_with_reload(
189 config: &LogConfig,
190 filter_layer: reload::Layer<EnvFilter, tracing_subscriber::Registry>,
191 span_events: FmtSpan,
192) -> Result<()> {
193 let registry = tracing_subscriber::registry().with(filter_layer);
194
195 let layer = fmt::layer()
196 .with_timer(UtcTime::rfc_3339())
197 .with_file(config.include_location)
198 .with_line_number(config.include_location)
199 .with_target(config.include_target)
200 .with_thread_ids(config.include_thread_ids)
201 .with_thread_names(config.include_thread_names)
202 .with_span_events(span_events)
203 .with_ansi(config.ansi_colors)
204 .with_writer(std::io::stderr);
205
206 registry.with(layer).init();
207 Ok(())
208}
209
210fn init_file_with_reload(
212 config: &LogConfig,
213 filter_layer: reload::Layer<EnvFilter, tracing_subscriber::Registry>,
214 span_events: FmtSpan,
215 directory: &Path,
216 filename_prefix: &str,
217 rotation_config: &super::rotation::RotationConfig,
218) -> Result<()> {
219 let rotation = match rotation_config.strategy {
220 RotationStrategy::Daily => Rotation::DAILY,
221 RotationStrategy::Hourly => Rotation::HOURLY,
222 RotationStrategy::Minutely => Rotation::MINUTELY,
223 RotationStrategy::Never => Rotation::NEVER,
224 };
225
226 let file_appender = RollingFileAppender::new(rotation, directory, filename_prefix);
227 let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
228
229 std::mem::forget(_guard);
232
233 let registry = tracing_subscriber::registry().with(filter_layer);
234
235 let layer = fmt::layer()
237 .json()
238 .with_timer(UtcTime::rfc_3339())
239 .with_file(config.include_location)
240 .with_line_number(config.include_location)
241 .with_target(config.include_target)
242 .with_thread_ids(config.include_thread_ids)
243 .with_thread_names(config.include_thread_names)
244 .with_span_events(span_events)
245 .with_ansi(false) .with_writer(non_blocking);
247
248 registry.with(layer).init();
249 Ok(())
250}
251
252fn init_multi_with_reload(
254 config: &LogConfig,
255 filter_layer: reload::Layer<EnvFilter, tracing_subscriber::Registry>,
256 span_events: FmtSpan,
257 targets: &[LogTarget],
258) -> Result<()> {
259 let has_console = targets.iter().any(|t| matches!(t, LogTarget::Stdout | LogTarget::Stderr));
263 let file_target = targets.iter().find_map(|t| {
264 if let LogTarget::File { directory, filename_prefix, rotation } = t {
265 Some((directory.clone(), filename_prefix.clone(), rotation.clone()))
266 } else {
267 None
268 }
269 });
270
271 let registry = tracing_subscriber::registry().with(filter_layer);
272
273 if has_console && file_target.is_some() {
274 let (directory, filename_prefix, rotation_config) = file_target.unwrap();
275
276 let rotation = match rotation_config.strategy {
277 RotationStrategy::Daily => Rotation::DAILY,
278 RotationStrategy::Hourly => Rotation::HOURLY,
279 RotationStrategy::Minutely => Rotation::MINUTELY,
280 RotationStrategy::Never => Rotation::NEVER,
281 };
282
283 let file_appender = RollingFileAppender::new(rotation, &directory, &filename_prefix);
284 let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
285 std::mem::forget(_guard);
286
287 let console_layer = fmt::layer()
289 .pretty()
290 .with_timer(UtcTime::rfc_3339())
291 .with_file(config.include_location)
292 .with_line_number(config.include_location)
293 .with_target(config.include_target)
294 .with_span_events(span_events.clone())
295 .with_ansi(config.ansi_colors)
296 .with_writer(std::io::stdout);
297
298 let file_layer = fmt::layer()
300 .json()
301 .with_timer(UtcTime::rfc_3339())
302 .with_file(config.include_location)
303 .with_line_number(config.include_location)
304 .with_target(config.include_target)
305 .with_span_events(span_events)
306 .with_ansi(false)
307 .with_writer(non_blocking);
308
309 registry.with(console_layer).with(file_layer).init();
310 } else if has_console {
311 init_stdout_with_reload(config, reload::Layer::new(build_env_filter(config)).0, span_events)?;
313 } else if let Some((directory, filename_prefix, rotation_config)) = file_target {
314 let reload_layer = reload::Layer::new(build_env_filter(config)).0;
316 init_file_with_reload(config, reload_layer, span_events, &directory, &filename_prefix, &rotation_config)?;
317 }
318
319 Ok(())
320}
321
322fn init_static(config: &LogConfig, filter: EnvFilter, span_events: FmtSpan) -> Result<()> {
324 match &config.target {
325 LogTarget::Stdout => init_stdout_static(config, filter, span_events),
326 LogTarget::Stderr => init_stderr_static(config, filter, span_events),
327 LogTarget::File {
328 directory,
329 filename_prefix,
330 rotation,
331 } => init_file_static(config, filter, span_events, directory, filename_prefix, rotation),
332 LogTarget::Multi(_) => {
333 init_with_dynamic_level(config, filter, span_events)
336 }
337 }
338}
339
340fn init_stdout_static(config: &LogConfig, filter: EnvFilter, span_events: FmtSpan) -> Result<()> {
342 let registry = tracing_subscriber::registry().with(filter);
343
344 match config.format {
345 LogFormat::Json => {
346 let layer = fmt::layer()
347 .json()
348 .with_timer(UtcTime::rfc_3339())
349 .with_file(config.include_location)
350 .with_line_number(config.include_location)
351 .with_target(config.include_target)
352 .with_thread_ids(config.include_thread_ids)
353 .with_thread_names(config.include_thread_names)
354 .with_span_events(span_events)
355 .with_writer(std::io::stdout);
356 registry.with(layer).init();
357 }
358 LogFormat::Pretty => {
359 let layer = fmt::layer()
360 .pretty()
361 .with_timer(UtcTime::rfc_3339())
362 .with_file(config.include_location)
363 .with_line_number(config.include_location)
364 .with_target(config.include_target)
365 .with_thread_ids(config.include_thread_ids)
366 .with_thread_names(config.include_thread_names)
367 .with_span_events(span_events)
368 .with_ansi(config.ansi_colors)
369 .with_writer(std::io::stdout);
370 registry.with(layer).init();
371 }
372 LogFormat::Compact => {
373 let layer = fmt::layer()
374 .compact()
375 .with_timer(UtcTime::rfc_3339())
376 .with_file(config.include_location)
377 .with_line_number(config.include_location)
378 .with_target(config.include_target)
379 .with_thread_ids(config.include_thread_ids)
380 .with_thread_names(config.include_thread_names)
381 .with_span_events(span_events)
382 .with_ansi(config.ansi_colors)
383 .with_writer(std::io::stdout);
384 registry.with(layer).init();
385 }
386 LogFormat::Full => {
387 let layer = fmt::layer()
388 .with_timer(UtcTime::rfc_3339())
389 .with_file(config.include_location)
390 .with_line_number(config.include_location)
391 .with_target(config.include_target)
392 .with_thread_ids(config.include_thread_ids)
393 .with_thread_names(config.include_thread_names)
394 .with_span_events(span_events)
395 .with_ansi(config.ansi_colors)
396 .with_writer(std::io::stdout);
397 registry.with(layer).init();
398 }
399 }
400
401 Ok(())
402}
403
404fn init_stderr_static(config: &LogConfig, filter: EnvFilter, span_events: FmtSpan) -> Result<()> {
406 let registry = tracing_subscriber::registry().with(filter);
407
408 let layer = fmt::layer()
409 .with_timer(UtcTime::rfc_3339())
410 .with_file(config.include_location)
411 .with_line_number(config.include_location)
412 .with_target(config.include_target)
413 .with_thread_ids(config.include_thread_ids)
414 .with_thread_names(config.include_thread_names)
415 .with_span_events(span_events)
416 .with_ansi(config.ansi_colors)
417 .with_writer(std::io::stderr);
418
419 registry.with(layer).init();
420 Ok(())
421}
422
423fn init_file_static(
425 config: &LogConfig,
426 filter: EnvFilter,
427 span_events: FmtSpan,
428 directory: &Path,
429 filename_prefix: &str,
430 rotation_config: &super::rotation::RotationConfig,
431) -> Result<()> {
432 let rotation = match rotation_config.strategy {
433 RotationStrategy::Daily => Rotation::DAILY,
434 RotationStrategy::Hourly => Rotation::HOURLY,
435 RotationStrategy::Minutely => Rotation::MINUTELY,
436 RotationStrategy::Never => Rotation::NEVER,
437 };
438
439 let file_appender = RollingFileAppender::new(rotation, directory, filename_prefix);
440 let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
441 std::mem::forget(_guard);
442
443 let registry = tracing_subscriber::registry().with(filter);
444
445 let layer = fmt::layer()
446 .json()
447 .with_timer(UtcTime::rfc_3339())
448 .with_file(config.include_location)
449 .with_line_number(config.include_location)
450 .with_target(config.include_target)
451 .with_thread_ids(config.include_thread_ids)
452 .with_thread_names(config.include_thread_names)
453 .with_span_events(span_events)
454 .with_ansi(false)
455 .with_writer(non_blocking);
456
457 registry.with(layer).init();
458 Ok(())
459}
460
461fn build_env_filter(config: &LogConfig) -> EnvFilter {
463 EnvFilter::try_from_default_env().unwrap_or_else(|_| {
465 let filter_str = config.build_filter_string();
466 EnvFilter::try_new(&filter_str).unwrap_or_else(|_| {
467 EnvFilter::new(config.level.as_filter_str())
468 })
469 })
470}
471
472fn build_span_events(config: &LogConfig) -> FmtSpan {
474 if config.include_span_events {
475 FmtSpan::ENTER | FmtSpan::EXIT
476 } else {
477 FmtSpan::NONE
478 }
479}
480
481pub fn init_test_logging() {
486 let _ = init_logging(&LogConfig::test());
487}
488
489pub fn is_logging_initialized() -> bool {
491 LOGGING_INITIALIZED.get().is_some()
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use super::super::config::LogLevel;
498
499 #[test]
500 fn test_build_env_filter() {
501 let config = LogConfig::builder().level(LogLevel::Debug).build();
502 let filter = build_env_filter(&config);
503 drop(filter);
505 }
506
507 #[test]
508 fn test_build_span_events() {
509 let config = LogConfig::builder().include_span_events(true).build();
510 let span_events = build_span_events(&config);
511 assert_ne!(span_events, FmtSpan::NONE);
513
514 let config = LogConfig::builder().include_span_events(false).build();
515 let span_events = build_span_events(&config);
516 assert_eq!(span_events, FmtSpan::NONE);
517 }
518}