netter_logger 0.1.1

Logger for Netter project
Documentation
use chrono::Local;
use colored::*;
use log::{Level, LevelFilter};
use std::path::{Path, PathBuf};
use std::io::ErrorKind;

const LOGS_PREFIX: &str = "netter_log";
const SEPARATOR: &str = "_";
const TIMESTAMP_FORMAT: &str = "%Y-%m-%d_%H-%M-%S";
const LOG_EXTENSION: &str = "log";
const CONSOLE_TIMESTAMP_FORMAT: &str = "%Y-%m-%d %H:%M:%S%.3f";

fn generate_filename_only() -> String {
    let now = Local::now();
    let timestamp = now.format(TIMESTAMP_FORMAT).to_string();
    format!("{}{}{}.{}", LOGS_PREFIX, SEPARATOR, timestamp, LOG_EXTENSION)
}

fn ensure_log_directory_exists(log_dir: &Path) -> std::io::Result<()> {
    if !log_dir.exists() {
        std::fs::create_dir_all(log_dir)?;
    }
    Ok(())
}

pub fn init(
    log_dir: Option<impl AsRef<Path>>,
    console_level: LevelFilter,
    file_level: LevelFilter,
) -> Result<(), fern::InitError> {

    let mut log_file_path: Option<PathBuf> = None;

    if let Some(dir) = log_dir {
        let dir_path = dir.as_ref();
        if let Err(e) = ensure_log_directory_exists(dir_path) {
             eprintln!("CRITICAL: Не удалось создать директорию для логов '{}': {}", dir_path.display(), e);
             return Err(fern::InitError::Io(std::io::Error::new(
                ErrorKind::Other,
                format!("Не удалось создать директорию для логов '{}': {}", dir_path.display(), e),
            )));
        }
        log_file_path = Some(dir_path.join(generate_filename_only()));
    }

    let console_dispatch = fern::Dispatch::new()
        .format(|out, message, record| {
            let level_str = match record.level() {
                Level::Error => "ERROR".red().bold(),
                Level::Warn => "WARN ".yellow().bold(),
                Level::Info => "INFO ".green().bold(),
                Level::Debug => "DEBUG".blue().bold(),
                Level::Trace => "TRACE".magenta().bold(),
            };

            let timestamp = Local::now().format(CONSOLE_TIMESTAMP_FORMAT).to_string();

            let thread_name = std::thread::current().name().unwrap_or("unnamed").to_string();
            let target = record.target();

            out.finish(format_args!(
                "[{}] [{}] [{}] [{}] {}",
                timestamp,
                level_str,
                thread_name,
                target,
                message
            ))
        })
        .level(console_level)
        .chain(std::io::stdout());

    let mut base_dispatch = fern::Dispatch::new()
        .level(LevelFilter::Trace)
        .chain(console_dispatch);

    if let Some(path) = &log_file_path {
        let file_dispatch = fern::Dispatch::new()
            .format(|out, message, record| {
                let timestamp = Local::now().format(CONSOLE_TIMESTAMP_FORMAT).to_string();
                 let thread_name = std::thread::current().name().unwrap_or("unnamed").to_string();
                 let target = record.target();
                out.finish(format_args!(
                    "[{}] [{:<5}] [{}] [{}] [{}:{}] {}",
                    timestamp,
                    record.level(),
                    thread_name,
                    target,
                    record.file().unwrap_or("?"),
                    record.line().unwrap_or(0),
                    message
                ))
            })
            .level(file_level)
            .chain(fern::log_file(path)?);

        base_dispatch = base_dispatch.chain(file_dispatch);
    }

    base_dispatch.apply()?;

    log::info!("Логгер инициализирован. Уровень консоли: {}, Уровень файла: {}", console_level, file_level);
    if let Some(path) = &log_file_path {
         log::info!("Запись логов в файл: {}", path.display());
    } else {
         log::info!("Запись логов в файл отключена.");
    }

    Ok(())
}