use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::{collections::HashMap, fmt::Debug, hash::Hash};
use tokio::sync::mpsc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
enum Tab {
Info,
Debug,
Error,
Silenced,
Dropped,
}
impl Tab {
fn as_str(&self) -> &'static str {
match self {
Tab::Info => "info",
Tab::Debug => "debug",
Tab::Error => "error",
Tab::Silenced => "silenced",
Tab::Dropped => "dropped",
}
}
fn file_name(&self) -> String {
format!("{}.log", self.as_str())
}
}
enum FileLogCommand {
AddMessage { tab: Tab, event: kerf::ArcEvent },
ClearLog(Tab),
ClearAllLogs,
}
struct FileLogger {
file: Arc<Mutex<File>>,
log_count: usize,
}
impl FileLogger {
fn new(path: PathBuf) -> io::Result<Self> {
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&path)?;
Ok(Self {
file: Arc::new(Mutex::new(file)),
log_count: 0,
})
}
fn handle_event(&mut self, event: kerf::ArcEvent) -> io::Result<()> {
let formatted = format!("[{}] {}\n", event.id, event.format());
{
let mut file = self.file.lock().unwrap();
file.write_all(formatted.as_bytes())?;
file.flush()?;
}
self.log_count += 1;
Ok(())
}
fn clear_log(&mut self) -> io::Result<()> {
let mut file = self.file.lock().unwrap();
file.set_len(0)?; file.flush()?;
self.log_count = 0;
Ok(())
}
}
struct FileTraceDemo {
loggers: HashMap<Tab, FileLogger>,
base_path: PathBuf,
}
impl FileTraceDemo {
fn new(base_path: PathBuf) -> io::Result<Self> {
let mut demo = Self {
loggers: HashMap::new(),
base_path,
};
for log_type in [
Tab::Info,
Tab::Debug,
Tab::Error,
Tab::Silenced,
Tab::Dropped,
] {
let log_path = demo.base_path.join(log_type.file_name());
let logger = FileLogger::new(log_path)?;
demo.loggers.insert(log_type, logger);
}
Ok(demo)
}
fn handle_message(&mut self, log_type: Tab, event: kerf::ArcEvent) -> io::Result<()> {
if let Some(logger) = self.loggers.get_mut(&log_type) {
logger.handle_event(event)?;
}
Ok(())
}
fn clear_log(&mut self, log_type: Tab) -> io::Result<()> {
if let Some(logger) = self.loggers.get_mut(&log_type) {
logger.clear_log()?;
}
Ok(())
}
fn clear_all_logs(&mut self) -> io::Result<()> {
for (_, logger) in self.loggers.iter_mut() {
logger.clear_log()?;
}
Ok(())
}
fn get_log_count(&self, log_type: Tab) -> usize {
self.loggers
.get(&log_type)
.map(|logger| logger.log_count)
.unwrap_or(0)
}
fn get_log_file_size(&self, log_type: Tab) -> io::Result<u64> {
let path = self.base_path.join(log_type.file_name());
let metadata = std::fs::metadata(path)?;
Ok(metadata.len())
}
}
#[tokio::main]
async fn main() -> Result<()> {
let temp_path = {
let temp_dir = tempfile::tempdir()?;
temp_dir.path().to_owned()
};
tokio::fs::create_dir_all(&temp_path).await?;
println!("=== LOGGING TO TEMPORARY DIRECTORY ===");
println!("Log directory: {}", temp_path.display());
println!("=======================================\n");
let tracer = kerf::Kerf::init(
kerf::Config::default_main()
.with_tab(Tab::Info.as_str(), kerf::Match::info().all_modules())
.with_tab(Tab::Debug.as_str(), kerf::Match::debug().all_modules())
.with_tab(Tab::Error.as_str(), kerf::Match::error().all_modules()),
)?;
let (tx, mut rx) = mpsc::unbounded_channel::<FileLogCommand>();
let file_trace_demo = Arc::new(Mutex::new(FileTraceDemo::new(temp_path.clone())?));
for log_type in [Tab::Info, Tab::Debug, Tab::Error] {
println!(
"{} log: {}",
log_type.as_str(),
temp_path.join(log_type.file_name()).display()
);
}
println!("\n");
let demo_clone = Arc::clone(&file_trace_demo);
tokio::spawn(async move {
while let Some(cmd) = rx.recv().await {
let mut demo = demo_clone.lock().unwrap();
match cmd {
FileLogCommand::AddMessage { tab, event } => {
if let Err(e) = demo.handle_message(tab, event) {
eprintln!("Error handling event for {}: {}", tab.as_str(), e);
}
}
FileLogCommand::ClearLog(log_type) => {
if let Err(e) = demo.clear_log(log_type) {
eprintln!("Error clearing {} log: {}", log_type.as_str(), e);
}
}
FileLogCommand::ClearAllLogs => {
if let Err(e) = demo.clear_all_logs() {
eprintln!("Error clearing all logs: {e}");
}
}
}
}
});
let tx_clone = tx.clone();
tracer
.set_callback(move |event, tab_names| {
for &tab_name in tab_names {
let tab = match tab_name {
"info" => Tab::Info,
"debug" => Tab::Debug,
"error" => Tab::Error,
_ => unreachable!(),
};
if let Err(e) = tx_clone.send(FileLogCommand::AddMessage {
tab,
event: Arc::clone(&event),
}) {
eprintln!("Failed to send captured event: {e}");
}
}
})?
.await??;
let tx_clone = tx.clone();
tracer
.set_silenced_callback(move |event, _silencers| {
if let Err(e) = tx_clone.send(FileLogCommand::AddMessage {
tab: Tab::Silenced,
event,
}) {
eprintln!("Failed to send silenced event: {e}");
}
})?
.await??;
let tx_clone = tx.clone();
tracer
.set_dropped_callback(move |event| {
if let Err(e) = tx_clone.send(FileLogCommand::AddMessage {
tab: Tab::Dropped,
event,
}) {
eprintln!("Failed to send dropped event: {e}");
}
})?
.await??;
tokio::spawn(async move {
let mut counter = 0;
loop {
tracing::trace!("events starting");
tracing::info!("Regular info event {}", counter);
tracing::trace!("fired first event");
tracing::debug!("Debug details for count {}", counter);
tracing::trace!("debug fired");
if counter % 3 == 0 {
tracing::warn!("Warning: counter is divisible by 3!");
}
if counter % 5 == 0 {
tracing::error!("Error: counter hit multiple of 5!");
}
counter += 1;
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
}
});
tokio::spawn(async move {
let mut counter = 0;
loop {
tracing::info!(
target: "background_task",
"Background task running {}",
counter
);
tracing::debug!(
target: "background_task",
"Background task details {}",
counter
);
if counter % 4 == 0 {
tracing::warn!(
target: "background_task",
"Background warning!"
);
}
counter += 1;
tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await;
}
});
let tx_clone = tx.clone();
tokio::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
println!("\n=== CLEARING INFO LOG ===");
if let Err(e) = tx_clone.send(FileLogCommand::ClearLog(Tab::Info)) {
eprintln!("Failed to send clear info log command: {e}");
}
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
println!("\n=== CLEARING ALL LOGS ===");
if let Err(e) = tx_clone.send(FileLogCommand::ClearAllLogs) {
eprintln!("Failed to send clear all logs command: {e}");
}
});
let demo_clone = Arc::clone(&file_trace_demo);
tokio::spawn(async move {
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
println!("\n=== LOG FILE STATS ===");
let demo = demo_clone.lock().unwrap();
for log_type in [
Tab::Info,
Tab::Debug,
Tab::Error,
Tab::Silenced,
Tab::Dropped,
] {
let size = demo.get_log_file_size(log_type).unwrap_or(0);
let count = demo.get_log_count(log_type);
println!(
"{} log: {} bytes, {} events",
log_type.as_str(),
size,
count
);
}
println!("=====================\n");
}
});
tokio::signal::ctrl_c().await?;
println!("Shutting down...");
println!("Logs remain available at: {}", temp_path.display());
Ok(())
}