use tracing_core::LevelFilter;
use tracing_subscriber::{registry::LookupSpan, Layer};
use super::{DisplayPreference, WriterConfig};
use crate::tracing_logging::{rolling_file_appender_impl, tracing_config::TracingConfig};
#[macro_export]
macro_rules! create_fmt {
() => {
tracing_subscriber::fmt::layer()
.compact()
.without_time()
.with_thread_ids(false)
.with_thread_names(false)
.with_target(false)
.with_file(false)
.with_line_number(false)
.with_ansi(true)
};
}
pub type DynLayer<S> = dyn Layer<S> + Send + Sync + 'static;
pub fn try_create_layers(
tracing_config: TracingConfig,
) -> miette::Result<Option<Vec<Box<DynLayer<tracing_subscriber::Registry>>>>> {
let layers = {
let mut return_it: Vec<Box<DynLayer<tracing_subscriber::Registry>>> = vec![];
return_it.push(Box::new(tracing_config.get_level_filter()));
let _ = try_create_display_layer(
tracing_config.get_level_filter(),
tracing_config.get_writer_config(),
)?
.map(|layer| return_it.push(layer));
let _ = try_create_file_layer(
tracing_config.get_level_filter(),
tracing_config.get_writer_config(),
)?
.map(|layer| return_it.push(layer));
return_it
};
Ok(Some(layers))
}
pub fn try_create_display_layer<S>(
level_filter: LevelFilter,
writer_config: WriterConfig,
) -> miette::Result<Option<Box<DynLayer<S>>>>
where
S: tracing_core::Subscriber,
for<'a> S: LookupSpan<'a>,
{
let fmt_layer = create_fmt!();
Ok(match writer_config {
WriterConfig::DisplayAndFile(display_pref, _)
| WriterConfig::Display(display_pref) => match display_pref {
DisplayPreference::Stdout => Some(Box::new(
fmt_layer
.with_writer(std::io::stdout)
.with_filter(level_filter),
)),
DisplayPreference::Stderr => Some(Box::new(
fmt_layer
.with_writer(std::io::stderr)
.with_filter(level_filter),
)),
DisplayPreference::SharedWriter(shared_writer) => {
let tracing_writer = move || -> Box<dyn std::io::Write> {
Box::new(shared_writer.clone())
};
Some(Box::new(
fmt_layer
.with_writer(tracing_writer)
.with_filter(level_filter),
))
}
},
_ => None,
})
}
pub fn try_create_file_layer<S>(
level_filter: LevelFilter,
writer_config: WriterConfig,
) -> miette::Result<Option<Box<DynLayer<S>>>>
where
S: tracing_core::Subscriber,
for<'a> S: LookupSpan<'a>,
{
let fmt_layer = create_fmt!();
Ok(match writer_config {
WriterConfig::DisplayAndFile(_, tracing_log_file_path_and_prefix)
| WriterConfig::File(tracing_log_file_path_and_prefix) => {
let file = rolling_file_appender_impl::try_create(
tracing_log_file_path_and_prefix.as_str(),
)?;
Some(Box::new(
fmt_layer.with_writer(file).with_filter(level_filter),
))
}
_ => None,
})
}
#[cfg(test)]
mod tests {
use tempfile::tempdir;
use super::*;
#[test]
fn test_try_create_display_layer() {
let level_filter = LevelFilter::DEBUG;
let writer_config = WriterConfig::Display(DisplayPreference::Stdout);
let layer: Option<Box<DynLayer<tracing_subscriber::Registry>>> =
try_create_display_layer(level_filter, writer_config).unwrap();
assert!(layer.is_some());
}
#[test]
fn test_try_create_file_layer() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("my_temp_log_file.log");
let file_path = file_path.to_str().unwrap().to_string();
println!("file_path: {}", file_path);
let level_filter = LevelFilter::DEBUG;
let writer_config = WriterConfig::File(file_path.clone());
let layer: Option<Box<DynLayer<tracing_subscriber::Registry>>> =
try_create_file_layer(level_filter, writer_config).unwrap();
assert!(layer.is_some());
assert!(std::path::Path::new(&file_path).exists());
}
#[test]
fn test_try_create_both_layers() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("my_temp_log_file.log");
let file_path = file_path.to_str().unwrap().to_string();
let tracing_config = TracingConfig {
writer_config: WriterConfig::DisplayAndFile(
DisplayPreference::Stdout,
file_path.clone(),
),
level_filter: LevelFilter::DEBUG,
};
let layers = try_create_layers(tracing_config).unwrap().unwrap();
assert_eq!(layers.len(), 3);
assert!(std::path::Path::new(&file_path).exists());
}
}
#[cfg(test)]
mod test_tracing_bin_stdio {
use assert_cmd::Command;
const EXPECTED: [&str; 4] = ["error", "warn", "info", "debug"];
#[test]
fn stdout() {
let output = Command::cargo_bin("tracing_test_bin")
.unwrap()
.arg("stdout")
.ok()
.unwrap();
let output = String::from_utf8_lossy(output.stdout.as_slice());
for it in EXPECTED.iter() {
assert!(output.contains(it));
}
}
#[test]
fn stderr() {
let output = Command::cargo_bin("tracing_test_bin")
.unwrap()
.arg("stderr")
.ok()
.unwrap();
let output = String::from_utf8_lossy(output.stderr.as_slice());
for it in EXPECTED.iter() {
assert!(output.contains(it));
}
}
}
#[cfg(test)]
mod test_tracing_shared_writer_output {
use super::*;
use crate::{LineStateControlSignal, SharedWriter};
const EXPECTED: [&str; 4] = ["error", "warn", "info", "debug"];
#[tokio::test]
#[allow(clippy::needless_return)]
async fn test_shared_writer_output() {
let (sender, mut receiver) = tokio::sync::mpsc::channel(1_000);
let display_pref = DisplayPreference::SharedWriter(SharedWriter::new(sender));
let default_guard = TracingConfig {
writer_config: WriterConfig::Display(display_pref),
level_filter: LevelFilter::DEBUG,
}
.install_thread_local()
.unwrap();
tracing::error!("error");
tracing::warn!("warn");
tracing::info!("info");
tracing::debug!("debug");
tracing::trace!("trace");
receiver.close();
let mut output = vec![];
while let Some(LineStateControlSignal::Line(line)) = receiver.recv().await {
output.push(String::from_utf8_lossy(&line).trim().to_string());
}
let output = output.join("\n");
println!("output: {}", output);
for it in EXPECTED.iter() {
assert!(output.contains(it));
}
drop(default_guard)
}
}