// Copyright 2021 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use fern::FormatCallback; use log::{logger, RecordBuilder}; use log::{LevelFilter, Record}; use serde::Serialize; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::borrow::Cow; use std::collections::HashMap; use std::{ fmt::Arguments, fs::{self, File}, iter::FromIterator, path::{Path, PathBuf}, }; use tauri::{ plugin::{self, TauriPlugin}, Manager, Runtime, }; pub use fern; const DEFAULT_MAX_FILE_SIZE: u128 = 40000; const DEFAULT_ROTATION_STRATEGY: RotationStrategy = RotationStrategy::KeepOne; const DEFAULT_LOG_TARGETS: [LogTarget; 2] = [LogTarget::Stdout, LogTarget::LogDir]; /// An enum representing the available verbosity levels of the logger. /// /// It is very similar to the [`log::Level`], but serializes to unsigned ints instead of strings. #[derive(Debug, Clone, Deserialize_repr, Serialize_repr)] #[repr(u16)] pub enum LogLevel { /// The "trace" level. /// /// Designates very low priority, often extremely verbose, information. Trace = 1, /// The "debug" level. /// /// Designates lower priority information. Debug, /// The "info" level. /// /// Designates useful information. Info, /// The "warn" level. /// /// Designates hazardous situations. Warn, /// The "error" level. /// /// Designates very serious errors. Error, } impl From for log::Level { fn from(log_level: LogLevel) -> Self { match log_level { LogLevel::Trace => log::Level::Trace, LogLevel::Debug => log::Level::Debug, LogLevel::Info => log::Level::Info, LogLevel::Warn => log::Level::Warn, LogLevel::Error => log::Level::Error, } } } impl From for LogLevel { fn from(log_level: log::Level) -> Self { match log_level { log::Level::Trace => LogLevel::Trace, log::Level::Debug => LogLevel::Debug, log::Level::Info => LogLevel::Info, log::Level::Warn => LogLevel::Warn, log::Level::Error => LogLevel::Error, } } } pub enum RotationStrategy { KeepAll, KeepOne, } #[derive(Debug, Serialize, Clone)] struct RecordPayload { message: String, level: LogLevel, } /// An enum representing the available targets of the logger. pub enum LogTarget { /// Print logs to stdout. Stdout, /// Print logs to stderr. Stderr, /// Write logs to the given directory. /// /// The plugin will ensure the directory exists before writing logs. Folder(PathBuf), /// Write logs to the OS specififc logs directory. /// /// ### Platform-specific /// /// |Platform | Value | Example | /// | ------- | --------------------------------------------- | ---------------------------------------------- | /// | Linux | `{configDir}/{bundleIdentifier}` | `/home/alice/.config/com.tauri.dev` | /// | macOS | `{homeDir}/Library/Logs/{bundleIdentifier}` | `/Users/Alice/Library/Logs/com.tauri.dev` | /// | Windows | `{configDir}/{bundleIdentifier}` | `C:\Users\Alice\AppData\Roaming\com.tauri.dev` | LogDir, /// Forward logs to the webview (via the `log://log` event). /// /// This requires the webview to subscribe to log events, via this plugins `attachConsole` function. Webview, } #[tauri::command] fn log( level: LogLevel, message: String, location: Option<&str>, file: Option<&str>, line: Option, key_values: Option>, ) { let location = location.unwrap_or("webview"); let mut builder = RecordBuilder::new(); builder .target(location) .level(level.into()) .file(file) .line(line); let key_values = key_values.unwrap_or_default(); let mut kv = HashMap::new(); for (k, v) in key_values.iter() { kv.insert(k.as_str(), v.as_str()); } builder.key_values(&kv); logger().log(&builder.args(format_args!("{message}")).build()); } pub struct LoggerBuilder { dispatch: fern::Dispatch, rotation_strategy: RotationStrategy, max_file_size: u128, targets: Vec, } impl Default for LoggerBuilder { fn default() -> Self { let format = time::format_description::parse("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]") .unwrap(); let dispatch = fern::Dispatch::new().format(move |out, message, record| { out.finish(format_args!( "{}[{}][{}] {}", time::OffsetDateTime::now_utc().format(&format).unwrap(), record.target(), record.level(), message )) }); Self { dispatch, rotation_strategy: DEFAULT_ROTATION_STRATEGY, max_file_size: DEFAULT_MAX_FILE_SIZE, targets: DEFAULT_LOG_TARGETS.into(), } } } impl LoggerBuilder { pub fn new() -> Self { Default::default() } pub fn rotation_strategy(mut self, rotation_strategy: RotationStrategy) -> Self { self.rotation_strategy = rotation_strategy; self } pub fn max_file_size(mut self, max_file_size: u128) -> Self { self.max_file_size = max_file_size; self } pub fn format(mut self, formatter: F) -> Self where F: Fn(FormatCallback, &Arguments, &Record) + Sync + Send + 'static, { self.dispatch = self.dispatch.format(formatter); self } pub fn level(mut self, level_filter: impl Into) -> Self { self.dispatch = self.dispatch.level(level_filter.into()); self } pub fn level_for(mut self, module: impl Into>, level: LevelFilter) -> Self { self.dispatch = self.dispatch.level_for(module, level); self } pub fn filter(mut self, filter: F) -> Self where F: Fn(&log::Metadata) -> bool + Send + Sync + 'static, { self.dispatch = self.dispatch.filter(filter); self } pub fn target(mut self, target: LogTarget) -> Self { self.targets.push(target); self } pub fn targets(mut self, targets: impl IntoIterator) -> Self { self.targets = Vec::from_iter(targets); self } #[cfg(feature = "colored")] pub fn with_colors(self, colors: fern::colors::ColoredLevelConfig) -> Self { let format = time::format_description::parse("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]") .unwrap(); self.format(move |out, message, record| { out.finish(format_args!( "{}[{}][{}] {}", time::OffsetDateTime::now_utc().format(&format).unwrap(), record.target(), colors.color(record.level()), message )) }) } pub fn build(mut self) -> TauriPlugin { plugin::Builder::new("log") .invoke_handler(tauri::generate_handler![log]) .setup(move |app_handle| { let app_name = &app_handle.package_info().name; // setup targets for target in &self.targets { self.dispatch = self.dispatch.chain(match target { LogTarget::Stdout => fern::Output::from(std::io::stdout()), LogTarget::Stderr => fern::Output::from(std::io::stderr()), LogTarget::Folder(path) => { if !path.exists() { fs::create_dir_all(&path).unwrap(); } fern::log_file(get_log_file_path( &path, app_name, &self.rotation_strategy, self.max_file_size, )?)? .into() } LogTarget::LogDir => { let path = app_handle.path_resolver().log_dir().unwrap(); if !path.exists() { fs::create_dir_all(&path).unwrap(); } fern::log_file(get_log_file_path( &path, app_name, &self.rotation_strategy, self.max_file_size, )?)? .into() } LogTarget::Webview => { let app_handle = app_handle.clone(); fern::Output::call(move |record| { let payload = RecordPayload { message: record.args().to_string(), level: record.level().into(), }; let app_handle = app_handle.clone(); tauri::async_runtime::spawn(async move { app_handle.emit_all("log://log", payload).unwrap(); }); }) } }); } self.dispatch.apply()?; Ok(()) }) .build() } } fn get_log_file_path( dir: &impl AsRef, app_name: &str, rotation_strategy: &RotationStrategy, max_file_size: u128, ) -> plugin::Result { let path = dir.as_ref().join(format!("{}.log", app_name)); if path.exists() { let log_size = File::open(&path)?.metadata()?.len() as u128; if log_size > max_file_size { match rotation_strategy { RotationStrategy::KeepAll => { let to = dir.as_ref().join(format!( "{}_{}.log", app_name, time::OffsetDateTime::now_utc() .format( &time::format_description::parse( "[year]-[month]-[day]_[hour]-[minute]-[second]" ) .unwrap() ) .unwrap(), )); if to.is_file() { // designated rotated log file name already exists // highly unlikely but defensively handle anyway by adding .bak to filename let mut to_bak = to.clone(); to_bak.set_file_name(format!( "{}.bak", to_bak.file_name().unwrap().to_string_lossy() )); fs::rename(&to, to_bak)?; } fs::rename(&path, to)?; } RotationStrategy::KeepOne => { fs::remove_file(&path)?; } } } } Ok(path) }