mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-08-05 18:18:37 +02:00
* dialog * fs * geolocation * haptic * log * single instance * notification * updater * ## * persisted scopes * stronghold * sql * upload * websocket * http (this one was too long that I generated through codex, didn't review yet) * add change file * no persisted-scope-js * add `://` * Merge branch 'v2' into document-cargo-features * update system-proxy docs * finish http docs * notification doesn't need version bumps * clean up docs
879 lines
30 KiB
Rust
879 lines
30 KiB
Rust
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//! Logging for Tauri applications.
|
|
//!
|
|
//! ## Cargo features
|
|
//!
|
|
//! - **colored**: Enables [`Builder::with_colors`] `fern`'s `colored` feature for ANSI-colored outputs.
|
|
//! - **tracing**: Emit both log and tracing for the JavaScript log commands.
|
|
|
|
#![doc(
|
|
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
|
|
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
|
|
)]
|
|
|
|
use fern::{Filter, FormatCallback};
|
|
use log::{LevelFilter, Record};
|
|
use serde::Serialize;
|
|
use serde_repr::{Deserialize_repr, Serialize_repr};
|
|
use std::borrow::Cow;
|
|
use std::fs::OpenOptions;
|
|
use std::io::Write;
|
|
use std::{
|
|
fmt::Arguments,
|
|
fs::{self, File},
|
|
iter::FromIterator,
|
|
path::{Path, PathBuf},
|
|
};
|
|
use tauri::{
|
|
plugin::{self, TauriPlugin},
|
|
Manager, Runtime,
|
|
};
|
|
use tauri::{AppHandle, Emitter};
|
|
use time::{macros::format_description, OffsetDateTime};
|
|
|
|
pub use fern;
|
|
pub use log;
|
|
|
|
mod commands;
|
|
|
|
pub const WEBVIEW_TARGET: &str = "webview";
|
|
|
|
#[cfg(target_os = "ios")]
|
|
mod ios {
|
|
swift_rs::swift!(pub fn tauri_log(
|
|
level: u8, message: *const std::ffi::c_void
|
|
));
|
|
}
|
|
|
|
const DEFAULT_MAX_FILE_SIZE: u64 = 40_000;
|
|
const DEFAULT_ROTATION_STRATEGY: RotationStrategy = RotationStrategy::KeepOne;
|
|
const DEFAULT_TIMEZONE_STRATEGY: TimezoneStrategy = TimezoneStrategy::UseUtc;
|
|
const DEFAULT_FILE_OPEN_STRATEGY: FileOpenStrategy = FileOpenStrategy::Append;
|
|
const DEFAULT_LOG_TARGETS: [Target; 2] = [
|
|
Target::new(TargetKind::Stdout),
|
|
Target::new(TargetKind::LogDir { file_name: None }),
|
|
];
|
|
const LOG_DATE_FORMAT: &[time::format_description::FormatItem<'_>] =
|
|
format_description!("[year]-[month]-[day]_[hour]-[minute]-[second]");
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum Error {
|
|
#[error(transparent)]
|
|
Tauri(#[from] tauri::Error),
|
|
#[error(transparent)]
|
|
Io(#[from] std::io::Error),
|
|
#[error(transparent)]
|
|
TimeFormat(#[from] time::error::Format),
|
|
#[error(transparent)]
|
|
InvalidFormatDescription(#[from] time::error::InvalidFormatDescription),
|
|
#[error("Internal logger disabled and cannot be acquired or attached")]
|
|
LoggerNotInitialized,
|
|
}
|
|
|
|
/// 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<LogLevel> 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<log::Level> 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,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum RotationStrategy {
|
|
/// Will keep all the logs, renaming them to include the date.
|
|
KeepAll,
|
|
/// Will only keep the most recent log up to its maximal size.
|
|
KeepOne,
|
|
/// Will keep some of the most recent logs, renaming them to include the date.
|
|
KeepSome(usize),
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum TimezoneStrategy {
|
|
UseUtc,
|
|
UseLocal,
|
|
}
|
|
|
|
impl TimezoneStrategy {
|
|
pub fn get_now(&self) -> OffsetDateTime {
|
|
match self {
|
|
TimezoneStrategy::UseUtc => OffsetDateTime::now_utc(),
|
|
TimezoneStrategy::UseLocal => {
|
|
OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc())
|
|
} // Fallback to UTC since Rust cannot determine local timezone
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum FileOpenStrategy {
|
|
/// Open existing file from last session and append, if any.
|
|
Append,
|
|
/// Create a new file on each session start, rotating the last session if any.
|
|
Rotate,
|
|
}
|
|
|
|
/// A custom log writer that rotates the log file when it exceeds specified size.
|
|
struct RotatingFile {
|
|
dir: PathBuf,
|
|
file_name: String,
|
|
path: PathBuf,
|
|
/// Maximum file size before rotating in bytes
|
|
max_size: u64,
|
|
/// Current file size in bytes
|
|
current_size: u64,
|
|
rotation_strategy: RotationStrategy,
|
|
timezone_strategy: TimezoneStrategy,
|
|
file_open_strategy: FileOpenStrategy,
|
|
inner: Option<File>,
|
|
buffer: Vec<u8>,
|
|
}
|
|
|
|
impl RotatingFile {
|
|
pub fn new(
|
|
dir: impl AsRef<Path>,
|
|
file_name: String,
|
|
max_size: u64,
|
|
rotation_strategy: RotationStrategy,
|
|
timezone_strategy: TimezoneStrategy,
|
|
file_open_strategy: FileOpenStrategy,
|
|
) -> Result<Self, Error> {
|
|
let dir = dir.as_ref().to_path_buf();
|
|
let path = dir.join(&file_name).with_extension("log");
|
|
|
|
let mut rotator = Self {
|
|
dir,
|
|
file_name,
|
|
path,
|
|
max_size,
|
|
current_size: 0,
|
|
rotation_strategy,
|
|
timezone_strategy,
|
|
file_open_strategy,
|
|
inner: None,
|
|
buffer: Vec::new(),
|
|
};
|
|
|
|
rotator.open_file()?;
|
|
if rotator.current_size >= rotator.max_size
|
|
|| (rotator.current_size > 0 && rotator.file_open_strategy == FileOpenStrategy::Rotate)
|
|
{
|
|
rotator.rotate()?;
|
|
}
|
|
if let RotationStrategy::KeepSome(keep_count) = rotator.rotation_strategy {
|
|
rotator.remove_old_files(keep_count)?;
|
|
}
|
|
|
|
Ok(rotator)
|
|
}
|
|
|
|
fn open_file(&mut self) -> Result<(), Error> {
|
|
let file = OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(&self.path)?;
|
|
self.current_size = file.metadata()?.len();
|
|
self.inner = Some(file);
|
|
Ok(())
|
|
}
|
|
|
|
fn rotate(&mut self) -> Result<(), Error> {
|
|
if let Some(mut file) = self.inner.take() {
|
|
let _ = file.flush();
|
|
}
|
|
if self.path.exists() {
|
|
match self.rotation_strategy {
|
|
RotationStrategy::KeepAll => {
|
|
self.rename_file_to_dated()?;
|
|
}
|
|
RotationStrategy::KeepSome(keep_count) => {
|
|
// remove_old_files excludes the active file.
|
|
// So we need to keep (keep_count - 1) archived files to make room for the one we are about to archive.
|
|
self.remove_old_files(keep_count - 1)?;
|
|
self.rename_file_to_dated()?;
|
|
}
|
|
RotationStrategy::KeepOne => {
|
|
fs::remove_file(&self.path)?;
|
|
}
|
|
}
|
|
}
|
|
self.open_file()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove old log files until the number of old log files is equal to the keep_count,
|
|
/// the current active log file is not included in the keep_count.
|
|
fn remove_old_files(&self, keep_count: usize) -> Result<(), Error> {
|
|
let mut files = fs::read_dir(&self.dir)?
|
|
.filter_map(|entry| {
|
|
let entry = entry.ok()?;
|
|
let path = entry.path();
|
|
let old_file_name = path.file_name()?.to_string_lossy().into_owned();
|
|
if old_file_name.starts_with(&self.file_name)
|
|
// exclude the current active file
|
|
&& old_file_name != format!("{}.log", self.file_name)
|
|
{
|
|
let date = old_file_name
|
|
.strip_prefix(&self.file_name)?
|
|
.strip_prefix("_")?
|
|
.strip_suffix(".log")?;
|
|
Some((path, date.to_string()))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
files.sort_by(|a, b| a.1.cmp(&b.1));
|
|
|
|
if files.len() > keep_count {
|
|
let files_to_remove = files.len() - keep_count;
|
|
for (old_log_path, _) in files.iter().take(files_to_remove) {
|
|
fs::remove_file(old_log_path)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn rename_file_to_dated(&self) -> Result<(), Error> {
|
|
let to = self.dir.join(format!(
|
|
"{}_{}.log",
|
|
self.file_name,
|
|
self.timezone_strategy
|
|
.get_now()
|
|
.format(LOG_DATE_FORMAT)
|
|
.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(&self.path, &to)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Write for RotatingFile {
|
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
|
self.buffer.extend_from_slice(buf);
|
|
Ok(buf.len())
|
|
}
|
|
|
|
fn flush(&mut self) -> std::io::Result<()> {
|
|
if self.buffer.is_empty() {
|
|
return Ok(());
|
|
}
|
|
if self.inner.is_none() {
|
|
self.open_file().map_err(std::io::Error::other)?;
|
|
}
|
|
|
|
if self.current_size != 0 && self.current_size + (self.buffer.len() as u64) > self.max_size
|
|
{
|
|
self.rotate().map_err(std::io::Error::other)?;
|
|
}
|
|
|
|
if let Some(file) = self.inner.as_mut() {
|
|
file.write_all(&self.buffer)?;
|
|
self.current_size += self.buffer.len() as u64;
|
|
file.flush()?;
|
|
}
|
|
self.buffer.clear();
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Clone)]
|
|
struct RecordPayload {
|
|
message: String,
|
|
level: LogLevel,
|
|
}
|
|
|
|
/// An enum representing the available targets of the logger.
|
|
pub enum TargetKind {
|
|
/// 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 {
|
|
path: PathBuf,
|
|
file_name: Option<String>,
|
|
},
|
|
/// Write logs to the OS specific logs directory.
|
|
///
|
|
/// ### Platform-specific
|
|
///
|
|
/// |Platform | Value | Example |
|
|
/// | --------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
|
/// | Linux | `$XDG_DATA_HOME/{bundleIdentifier}/logs` or `$HOME/.local/share/{bundleIdentifier}/logs` | `/home/alice/.local/share/com.tauri.dev/logs` |
|
|
/// | macOS/iOS | `{homeDir}/Library/Logs/{bundleIdentifier}` | `/Users/Alice/Library/Logs/com.tauri.dev` |
|
|
/// | Windows | `{FOLDERID_LocalAppData}/{bundleIdentifier}/logs` | `C:\Users\Alice\AppData\Local\com.tauri.dev\logs` |
|
|
/// | Android | `{ConfigDir}/logs` | `/data/data/com.tauri.dev/files/logs` |
|
|
LogDir { file_name: Option<String> },
|
|
/// 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,
|
|
/// Send logs to a [`fern::Dispatch`]
|
|
///
|
|
/// You can use this to construct arbitrary log targets.
|
|
Dispatch(fern::Dispatch),
|
|
}
|
|
|
|
type Formatter = dyn Fn(FormatCallback, &Arguments, &Record) + Send + Sync + 'static;
|
|
|
|
/// A log target.
|
|
pub struct Target {
|
|
kind: TargetKind,
|
|
filters: Vec<Box<Filter>>,
|
|
formatter: Option<Box<Formatter>>,
|
|
}
|
|
|
|
impl Target {
|
|
#[inline]
|
|
pub const fn new(kind: TargetKind) -> Self {
|
|
Self {
|
|
kind,
|
|
filters: Vec::new(),
|
|
formatter: None,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn filter<F>(mut self, filter: F) -> Self
|
|
where
|
|
F: Fn(&log::Metadata) -> bool + Send + Sync + 'static,
|
|
{
|
|
self.filters.push(Box::new(filter));
|
|
self
|
|
}
|
|
|
|
#[inline]
|
|
pub fn format<F>(mut self, formatter: F) -> Self
|
|
where
|
|
F: Fn(FormatCallback, &Arguments, &Record) + Send + Sync + 'static,
|
|
{
|
|
self.formatter.replace(Box::new(formatter));
|
|
self
|
|
}
|
|
}
|
|
|
|
pub struct Builder {
|
|
dispatch: fern::Dispatch,
|
|
rotation_strategy: RotationStrategy,
|
|
timezone_strategy: TimezoneStrategy,
|
|
file_open_strategy: FileOpenStrategy,
|
|
max_file_size: u128,
|
|
targets: Vec<Target>,
|
|
is_skip_logger: bool,
|
|
}
|
|
|
|
impl Default for Builder {
|
|
fn default() -> Self {
|
|
#[cfg(desktop)]
|
|
let format = format_description!("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]");
|
|
let dispatch = fern::Dispatch::new().format(move |out, message, record| {
|
|
out.finish(
|
|
#[cfg(mobile)]
|
|
format_args!("[{}] {}", record.target(), message),
|
|
#[cfg(desktop)]
|
|
format_args!(
|
|
"{}[{}][{}] {}",
|
|
DEFAULT_TIMEZONE_STRATEGY.get_now().format(&format).unwrap(),
|
|
record.target(),
|
|
record.level(),
|
|
message
|
|
),
|
|
)
|
|
});
|
|
Self {
|
|
dispatch,
|
|
rotation_strategy: DEFAULT_ROTATION_STRATEGY,
|
|
timezone_strategy: DEFAULT_TIMEZONE_STRATEGY,
|
|
file_open_strategy: DEFAULT_FILE_OPEN_STRATEGY,
|
|
max_file_size: DEFAULT_MAX_FILE_SIZE as u128,
|
|
targets: DEFAULT_LOG_TARGETS.into(),
|
|
is_skip_logger: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Builder {
|
|
pub fn new() -> Self {
|
|
Default::default()
|
|
}
|
|
|
|
/// Sets the [`RotationStrategy`].
|
|
///
|
|
/// Default is [`RotationStrategy::KeepOne`]
|
|
pub fn rotation_strategy(mut self, rotation_strategy: RotationStrategy) -> Self {
|
|
self.rotation_strategy = rotation_strategy;
|
|
self
|
|
}
|
|
|
|
/// Sets the [`TimezoneStrategy`].
|
|
/// Calling this method overrides the format set in [`Self::format`].
|
|
///
|
|
/// Default is [`TimezoneStrategy::UseUtc`]
|
|
pub fn timezone_strategy(mut self, timezone_strategy: TimezoneStrategy) -> Self {
|
|
self.timezone_strategy = timezone_strategy.clone();
|
|
|
|
let format = format_description!("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]");
|
|
self.dispatch = self.dispatch.format(move |out, message, record| {
|
|
out.finish(format_args!(
|
|
"{}[{}][{}] {}",
|
|
timezone_strategy.get_now().format(&format).unwrap(),
|
|
record.level(),
|
|
record.target(),
|
|
message
|
|
))
|
|
});
|
|
self
|
|
}
|
|
|
|
/// Sets the strategy to open the log file.
|
|
///
|
|
/// The default is [`FileOpenStrategy::Append`].
|
|
pub fn file_open_strategy(mut self, file_open_strategy: FileOpenStrategy) -> Self {
|
|
self.file_open_strategy = file_open_strategy;
|
|
self
|
|
}
|
|
|
|
/// Sets the maximum file size in bytes for log rotation.
|
|
///
|
|
/// Values larger than [`u64::MAX`] will be clamped to [`u64::MAX`].
|
|
/// In v3, this parameter will be changed to `u64`.
|
|
///
|
|
/// Default is `40_000`
|
|
pub fn max_file_size(mut self, max_file_size: u128) -> Self {
|
|
self.max_file_size = max_file_size.min(u64::MAX as u128);
|
|
self
|
|
}
|
|
|
|
/// Clears the format so that only the message is logged.
|
|
///
|
|
/// e.g. `log::info!("message")` will log out `message`
|
|
pub fn clear_format(mut self) -> Self {
|
|
self.dispatch = self.dispatch.format(|out, message, _record| {
|
|
out.finish(format_args!("{message}"));
|
|
});
|
|
self
|
|
}
|
|
|
|
/// Sets the formatter of this dispatch. The closure should accept a
|
|
/// callback, a message and a log record, and write the resulting
|
|
/// format to the writer.
|
|
///
|
|
/// The log record is passed for completeness, but the `args()` method of
|
|
/// the record should be ignored, and the [`std::fmt::Arguments`] given
|
|
/// should be used instead. `record.args()` may be used to retrieve the
|
|
/// _original_ log message, but in order to allow for true log
|
|
/// chaining, formatters should use the given message instead whenever
|
|
/// including the message in the output.
|
|
///
|
|
/// To avoid all allocation of intermediate results, the formatter is
|
|
/// "completed" by calling a callback, which then calls the rest of the
|
|
/// logging chain with the new formatted message. The callback object keeps
|
|
/// track of if it was called or not via a stack boolean as well, so if
|
|
/// you don't use `out.finish` the log message will continue down
|
|
/// the logger chain unformatted.
|
|
///
|
|
/// Example usage:
|
|
///
|
|
/// ```
|
|
/// tauri_plugin_log::Builder::new()
|
|
/// .format(|out, message, record| {
|
|
/// out.finish(format_args!(
|
|
/// "[{} {}] {}",
|
|
/// record.level(),
|
|
/// record.target(),
|
|
/// message
|
|
/// ))
|
|
/// });
|
|
/// ```
|
|
pub fn format<F>(mut self, formatter: F) -> Self
|
|
where
|
|
F: Fn(FormatCallback, &Arguments, &Record) + Sync + Send + 'static,
|
|
{
|
|
self.dispatch = self.dispatch.format(formatter);
|
|
self
|
|
}
|
|
|
|
/// Sets the overarching level filter for this logger.
|
|
/// All messages not already filtered by something set by [`Self::level_for`] will be affected.
|
|
///
|
|
/// All messages filtered will be discarded if less severe than the given level.
|
|
///
|
|
/// Default level is [`log::LevelFilter::Trace`].
|
|
pub fn level(mut self, level_filter: impl Into<LevelFilter>) -> Self {
|
|
self.dispatch = self.dispatch.level(level_filter.into());
|
|
self
|
|
}
|
|
|
|
/// Sets a per-target log level filter. Default target for log messages is
|
|
/// `crate_name::module_name` or
|
|
/// `crate_name` for logs in the crate root. Targets can also be set with
|
|
/// `info!(target: "target-name", ...)`.
|
|
///
|
|
/// For each log record fern will first try to match the most specific
|
|
/// level_for, and then progressively more general ones until either a
|
|
/// matching level is found, or the default level is used.
|
|
///
|
|
/// For example, a log for the target `hyper::http::h1` will first test a
|
|
/// level_for for `hyper::http::h1`, then for `hyper::http`, then for
|
|
/// `hyper`, then use the default level.
|
|
///
|
|
/// Examples:
|
|
///
|
|
/// A program wants to include a lot of debugging output, but the library
|
|
/// "hyper" is known to work well, so debug output from it should be
|
|
/// excluded:
|
|
///
|
|
/// ```
|
|
/// # fn main() {
|
|
/// tauri_plugin_log::Builder::new()
|
|
/// .level(log::LevelFilter::Trace)
|
|
/// .level_for("hyper", log::LevelFilter::Info)
|
|
/// # ;
|
|
/// # }
|
|
/// ```
|
|
pub fn level_for(mut self, module: impl Into<Cow<'static, str>>, level: LevelFilter) -> Self {
|
|
self.dispatch = self.dispatch.level_for(module, level);
|
|
self
|
|
}
|
|
|
|
/// Adds a custom filter which can reject messages passing through this logger.
|
|
///
|
|
/// [`Self::level`] and [`Self::level_for`] are preferred if applicable.
|
|
///
|
|
/// Example usage:
|
|
///
|
|
/// ```
|
|
/// # fn main() {
|
|
/// tauri_plugin_log::Builder::new()
|
|
/// .level(log::LevelFilter::Info)
|
|
/// .filter(|metadata| {
|
|
/// // Reject messages with the `Error` log level.
|
|
/// metadata.level() != log::LevelFilter::Error
|
|
/// })
|
|
/// # }
|
|
pub fn filter<F>(mut self, filter: F) -> Self
|
|
where
|
|
F: Fn(&log::Metadata) -> bool + Send + Sync + 'static,
|
|
{
|
|
self.dispatch = self.dispatch.filter(filter);
|
|
self
|
|
}
|
|
|
|
/// Removes all targets. Useful to ignore the default targets and reconfigure them.
|
|
pub fn clear_targets(mut self) -> Self {
|
|
self.targets.clear();
|
|
self
|
|
}
|
|
|
|
/// Adds a log target to the logger.
|
|
///
|
|
/// ```rust
|
|
/// use tauri_plugin_log::{Target, TargetKind};
|
|
/// tauri_plugin_log::Builder::new()
|
|
/// .target(Target::new(TargetKind::Webview));
|
|
/// ```
|
|
///
|
|
/// The default targets are
|
|
///
|
|
/// ```rust
|
|
/// # use tauri_plugin_log::{Target, TargetKind, Builder};
|
|
/// # Builder::new()
|
|
/// # .targets(
|
|
/// [
|
|
/// Target::new(TargetKind::Stdout),
|
|
/// Target::new(TargetKind::LogDir { file_name: None }),
|
|
/// ]
|
|
/// # );
|
|
/// ```
|
|
pub fn target(mut self, target: Target) -> Self {
|
|
self.targets.push(target);
|
|
self
|
|
}
|
|
|
|
/// Skip the creation and global registration of a logger
|
|
///
|
|
/// If you wish to use your own global logger, you must call `skip_logger` so that the plugin does not attempt to set a second global logger.
|
|
/// In this configuration, no logger will be created and the plugin's `log` command will rely on the result of `log::logger()`.
|
|
/// You will be responsible for configuring the logger yourself and any included targets will be ignored.
|
|
/// If ever initializing the plugin multiple times, such as if registering the plugin while testing, call this method to avoid panicking when registering multiple loggers.
|
|
/// For interacting with `tracing`, you can leverage the `tracing-log` logger to forward logs to `tracing` or enable the `tracing` feature for this plugin to emit events directly to the tracing system.
|
|
/// Both scenarios require calling this method.
|
|
///
|
|
/// ```rust
|
|
/// static LOGGER: SimpleLogger = SimpleLogger;
|
|
///
|
|
/// log::set_logger(&SimpleLogger)?;
|
|
/// log::set_max_level(LevelFilter::Info);
|
|
/// tauri_plugin_log::Builder::new()
|
|
/// .skip_logger();
|
|
/// ```
|
|
pub fn skip_logger(mut self) -> Self {
|
|
self.is_skip_logger = true;
|
|
self
|
|
}
|
|
|
|
/// Replaces the targets of the logger.
|
|
///
|
|
/// ```rust
|
|
/// use tauri_plugin_log::{Target, TargetKind, WEBVIEW_TARGET};
|
|
/// tauri_plugin_log::Builder::new()
|
|
/// .targets([
|
|
/// Target::new(TargetKind::Webview),
|
|
/// Target::new(TargetKind::LogDir { file_name: Some("webview".into()) }).filter(|metadata| metadata.target().starts_with(WEBVIEW_TARGET)),
|
|
/// Target::new(TargetKind::LogDir { file_name: Some("rust".into()) }).filter(|metadata| !metadata.target().starts_with(WEBVIEW_TARGET)),
|
|
/// ]);
|
|
/// ```
|
|
///
|
|
/// The default targets are
|
|
///
|
|
/// ```rust
|
|
/// # use tauri_plugin_log::{Target, TargetKind, Builder};
|
|
/// # Builder::new()
|
|
/// # .targets(
|
|
/// [
|
|
/// Target::new(TargetKind::Stdout),
|
|
/// Target::new(TargetKind::LogDir { file_name: None }),
|
|
/// ]
|
|
/// # );
|
|
/// ```
|
|
pub fn targets(mut self, targets: impl IntoIterator<Item = Target>) -> Self {
|
|
self.targets = Vec::from_iter(targets);
|
|
self
|
|
}
|
|
|
|
#[cfg(feature = "colored")]
|
|
pub fn with_colors(self, colors: fern::colors::ColoredLevelConfig) -> Self {
|
|
let format = format_description!("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]");
|
|
|
|
let timezone_strategy = self.timezone_strategy.clone();
|
|
self.format(move |out, message, record| {
|
|
out.finish(format_args!(
|
|
"{}[{}][{}] {}",
|
|
timezone_strategy.get_now().format(&format).unwrap(),
|
|
colors.color(record.level()),
|
|
record.target(),
|
|
message
|
|
))
|
|
})
|
|
}
|
|
|
|
fn acquire_logger<R: Runtime>(
|
|
app_handle: &AppHandle<R>,
|
|
mut dispatch: fern::Dispatch,
|
|
rotation_strategy: RotationStrategy,
|
|
timezone_strategy: TimezoneStrategy,
|
|
file_open_strategy: FileOpenStrategy,
|
|
max_file_size: u64,
|
|
targets: Vec<Target>,
|
|
) -> Result<(log::LevelFilter, Box<dyn log::Log>), Error> {
|
|
let app_name = &app_handle.package_info().name;
|
|
|
|
// setup targets
|
|
for target in targets {
|
|
let mut target_dispatch = fern::Dispatch::new();
|
|
for filter in target.filters {
|
|
target_dispatch = target_dispatch.filter(filter);
|
|
}
|
|
if let Some(formatter) = target.formatter {
|
|
target_dispatch = target_dispatch.format(formatter);
|
|
}
|
|
|
|
let logger = match target.kind {
|
|
#[cfg(target_os = "android")]
|
|
TargetKind::Stdout | TargetKind::Stderr => fern::Output::call(android_logger::log),
|
|
#[cfg(target_os = "ios")]
|
|
TargetKind::Stdout | TargetKind::Stderr => fern::Output::call(move |record| {
|
|
let message = format!("{}", record.args());
|
|
unsafe {
|
|
ios::tauri_log(
|
|
match record.level() {
|
|
log::Level::Trace | log::Level::Debug => 1,
|
|
log::Level::Info => 2,
|
|
log::Level::Warn | log::Level::Error => 3,
|
|
},
|
|
// The string is allocated in rust, so we must
|
|
// autorelease it rust to give it to the Swift
|
|
// runtime.
|
|
objc2::rc::Retained::autorelease_ptr(
|
|
objc2_foundation::NSString::from_str(message.as_str()),
|
|
) as _,
|
|
);
|
|
}
|
|
}),
|
|
#[cfg(desktop)]
|
|
TargetKind::Stdout => std::io::stdout().into(),
|
|
#[cfg(desktop)]
|
|
TargetKind::Stderr => std::io::stderr().into(),
|
|
TargetKind::Folder { path, file_name } => {
|
|
if !path.exists() {
|
|
fs::create_dir_all(&path)?;
|
|
}
|
|
|
|
let rotator = RotatingFile::new(
|
|
&path,
|
|
file_name.unwrap_or(app_name.clone()),
|
|
max_file_size,
|
|
rotation_strategy.clone(),
|
|
timezone_strategy.clone(),
|
|
file_open_strategy.clone(),
|
|
)?;
|
|
fern::Output::writer(Box::new(rotator), "\n")
|
|
}
|
|
TargetKind::LogDir { file_name } => {
|
|
let path = app_handle.path().app_log_dir()?;
|
|
if !path.exists() {
|
|
fs::create_dir_all(&path)?;
|
|
}
|
|
|
|
let rotator = RotatingFile::new(
|
|
&path,
|
|
file_name.unwrap_or(app_name.clone()),
|
|
max_file_size,
|
|
rotation_strategy.clone(),
|
|
timezone_strategy.clone(),
|
|
file_open_strategy.clone(),
|
|
)?;
|
|
fern::Output::writer(Box::new(rotator), "\n")
|
|
}
|
|
TargetKind::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 {
|
|
let _ = app_handle.emit("log://log", payload);
|
|
});
|
|
})
|
|
}
|
|
TargetKind::Dispatch(dispatch) => dispatch.into(),
|
|
};
|
|
target_dispatch = target_dispatch.chain(logger);
|
|
|
|
dispatch = dispatch.chain(target_dispatch);
|
|
}
|
|
|
|
Ok(dispatch.into_log())
|
|
}
|
|
|
|
fn plugin_builder<R: Runtime>() -> plugin::Builder<R> {
|
|
plugin::Builder::new("log").invoke_handler(tauri::generate_handler![commands::log])
|
|
}
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
pub fn split<R: Runtime>(
|
|
self,
|
|
app_handle: &AppHandle<R>,
|
|
) -> Result<(TauriPlugin<R>, log::LevelFilter, Box<dyn log::Log>), Error> {
|
|
if self.is_skip_logger {
|
|
return Err(Error::LoggerNotInitialized);
|
|
}
|
|
let plugin = Self::plugin_builder();
|
|
let (max_level, log) = Self::acquire_logger(
|
|
app_handle,
|
|
self.dispatch,
|
|
self.rotation_strategy,
|
|
self.timezone_strategy,
|
|
self.file_open_strategy,
|
|
self.max_file_size as u64,
|
|
self.targets,
|
|
)?;
|
|
|
|
Ok((plugin.build(), max_level, log))
|
|
}
|
|
|
|
pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
|
|
Self::plugin_builder()
|
|
.setup(move |app_handle, _api| {
|
|
if !self.is_skip_logger {
|
|
let (max_level, log) = Self::acquire_logger(
|
|
app_handle,
|
|
self.dispatch,
|
|
self.rotation_strategy,
|
|
self.timezone_strategy,
|
|
self.file_open_strategy,
|
|
self.max_file_size as u64,
|
|
self.targets,
|
|
)?;
|
|
attach_logger(max_level, log)?;
|
|
}
|
|
Ok(())
|
|
})
|
|
.build()
|
|
}
|
|
}
|
|
|
|
/// Attaches the given logger
|
|
pub fn attach_logger(
|
|
max_level: log::LevelFilter,
|
|
log: Box<dyn log::Log>,
|
|
) -> Result<(), log::SetLoggerError> {
|
|
log::set_boxed_logger(log)?;
|
|
log::set_max_level(max_level);
|
|
Ok(())
|
|
}
|