Skip to main content

tauri_plugin_log/
lib.rs

1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Logging for Tauri applications.
6//!
7//! ## Cargo features
8//!
9//! - **colored**: Enables `Builder::with_colors`, which uses `fern`'s `colored` feature for ANSI-colored outputs.
10//! - **tracing**: Emit both log and tracing for the JavaScript log commands.
11
12#![doc(
13    html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
14    html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
15)]
16
17use fern::{Filter, FormatCallback};
18use log::{LevelFilter, Record};
19use serde::Serialize;
20use serde_repr::{Deserialize_repr, Serialize_repr};
21use std::borrow::Cow;
22use std::fs::OpenOptions;
23use std::io::Write;
24use std::{
25    fmt::Arguments,
26    fs::{self, File},
27    iter::FromIterator,
28    path::{Path, PathBuf},
29};
30use tauri::{AppHandle, Emitter};
31use tauri::{
32    Manager, Runtime,
33    plugin::{self, TauriPlugin},
34};
35use time::{OffsetDateTime, macros::format_description};
36
37pub use fern;
38pub use log;
39
40mod commands;
41
42/// The log target prefix used for log records emitted through the JavaScript `trace`/`debug`/`info`/`warn`/`error`
43/// bindings, optionally followed by `::{location}` when the caller's source location could be determined.
44pub const WEBVIEW_TARGET: &str = "webview";
45
46#[cfg(target_os = "ios")]
47mod ios {
48    swift_rs::swift!(pub fn tauri_log(
49      level: u8, message: *const std::ffi::c_void
50    ));
51}
52
53const DEFAULT_MAX_FILE_SIZE: u64 = 40_000;
54const DEFAULT_ROTATION_STRATEGY: RotationStrategy = RotationStrategy::KeepOne;
55const DEFAULT_TIMEZONE_STRATEGY: TimezoneStrategy = TimezoneStrategy::UseUtc;
56const DEFAULT_FILE_OPEN_STRATEGY: FileOpenStrategy = FileOpenStrategy::Append;
57const DEFAULT_LOG_TARGETS: [Target; 2] = [
58    Target::new(TargetKind::Stdout),
59    Target::new(TargetKind::LogDir { file_name: None }),
60];
61const LOG_DATE_FORMAT: &[time::format_description::FormatItem<'_>] =
62    format_description!("[year]-[month]-[day]_[hour]-[minute]-[second]");
63
64/// The error type returned by this plugin's fallible operations.
65#[derive(Debug, thiserror::Error)]
66pub enum Error {
67    /// Forwarded from a call into the Tauri API, e.g. resolving the app log directory.
68    #[error(transparent)]
69    Tauri(#[from] tauri::Error),
70    /// Forwarded from a filesystem operation performed while creating, writing to or rotating a log file.
71    #[error(transparent)]
72    Io(#[from] std::io::Error),
73    /// Forwarded from formatting a timestamp with the configured [`TimezoneStrategy`].
74    #[error(transparent)]
75    TimeFormat(#[from] time::error::Format),
76    /// Forwarded from parsing an invalid `time` format description.
77    #[error(transparent)]
78    InvalidFormatDescription(#[from] time::error::InvalidFormatDescription),
79    /// Returned by [`Builder::split`] when [`Builder::skip_logger`] was called on the builder, since no logger was
80    /// created for it to return.
81    #[error("Internal logger disabled and cannot be acquired or attached")]
82    LoggerNotInitialized,
83}
84
85/// An enum representing the available verbosity levels of the logger.
86///
87/// It is very similar to the [`log::Level`], but serializes to unsigned ints instead of strings.
88#[derive(Debug, Clone, Deserialize_repr, Serialize_repr)]
89#[repr(u16)]
90pub enum LogLevel {
91    /// The "trace" level.
92    ///
93    /// Designates very low priority, often extremely verbose, information.
94    Trace = 1,
95    /// The "debug" level.
96    ///
97    /// Designates lower priority information.
98    Debug,
99    /// The "info" level.
100    ///
101    /// Designates useful information.
102    Info,
103    /// The "warn" level.
104    ///
105    /// Designates hazardous situations.
106    Warn,
107    /// The "error" level.
108    ///
109    /// Designates very serious errors.
110    Error,
111}
112
113impl From<LogLevel> for log::Level {
114    fn from(log_level: LogLevel) -> Self {
115        match log_level {
116            LogLevel::Trace => log::Level::Trace,
117            LogLevel::Debug => log::Level::Debug,
118            LogLevel::Info => log::Level::Info,
119            LogLevel::Warn => log::Level::Warn,
120            LogLevel::Error => log::Level::Error,
121        }
122    }
123}
124
125impl From<log::Level> for LogLevel {
126    fn from(log_level: log::Level) -> Self {
127        match log_level {
128            log::Level::Trace => LogLevel::Trace,
129            log::Level::Debug => LogLevel::Debug,
130            log::Level::Info => LogLevel::Info,
131            log::Level::Warn => LogLevel::Warn,
132            log::Level::Error => LogLevel::Error,
133        }
134    }
135}
136
137/// The strategy applied to a log file when it exceeds [`Builder::max_file_size`].
138#[derive(Debug, Clone)]
139pub enum RotationStrategy {
140    /// Will keep all the logs, renaming them to include the date.
141    KeepAll,
142    /// Will only keep the most recent log up to its maximal size.
143    KeepOne,
144    /// Will keep some of the most recent logs, renaming them to include the date.
145    KeepSome(usize),
146}
147
148/// The timezone used to compute the current time for log message timestamps and rotated log file names.
149#[derive(Debug, Clone)]
150pub enum TimezoneStrategy {
151    /// Use UTC time.
152    UseUtc,
153    /// Use the local timezone. Falls back to UTC if the local offset cannot be determined.
154    UseLocal,
155}
156
157impl TimezoneStrategy {
158    /// Returns the current time according to this strategy.
159    pub fn get_now(&self) -> OffsetDateTime {
160        match self {
161            TimezoneStrategy::UseUtc => OffsetDateTime::now_utc(),
162            TimezoneStrategy::UseLocal => {
163                OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc())
164            } // Fallback to UTC since Rust cannot determine local timezone
165        }
166    }
167}
168
169/// The strategy used to open a log file when a [`TargetKind::Folder`] or [`TargetKind::LogDir`] target is set up.
170#[derive(Debug, Clone, PartialEq)]
171pub enum FileOpenStrategy {
172    /// Open existing file from last session and append, if any.
173    Append,
174    /// Create a new file on each session start, rotating the last session if any.
175    Rotate,
176}
177
178/// A custom log writer that rotates the log file when it exceeds specified size.
179struct RotatingFile {
180    dir: PathBuf,
181    file_name: String,
182    path: PathBuf,
183    /// Maximum file size before rotating in bytes
184    max_size: u64,
185    /// Current file size in bytes
186    current_size: u64,
187    rotation_strategy: RotationStrategy,
188    timezone_strategy: TimezoneStrategy,
189    file_open_strategy: FileOpenStrategy,
190    inner: Option<File>,
191    buffer: Vec<u8>,
192}
193
194impl RotatingFile {
195    pub fn new(
196        dir: impl AsRef<Path>,
197        file_name: String,
198        max_size: u64,
199        rotation_strategy: RotationStrategy,
200        timezone_strategy: TimezoneStrategy,
201        file_open_strategy: FileOpenStrategy,
202    ) -> Result<Self, Error> {
203        let dir = dir.as_ref().to_path_buf();
204        let path = dir.join(&file_name).with_extension("log");
205
206        let mut rotator = Self {
207            dir,
208            file_name,
209            path,
210            max_size,
211            current_size: 0,
212            rotation_strategy,
213            timezone_strategy,
214            file_open_strategy,
215            inner: None,
216            buffer: Vec::new(),
217        };
218
219        rotator.open_file()?;
220        if rotator.current_size >= rotator.max_size
221            || (rotator.current_size > 0 && rotator.file_open_strategy == FileOpenStrategy::Rotate)
222        {
223            rotator.rotate()?;
224        }
225        if let RotationStrategy::KeepSome(keep_count) = rotator.rotation_strategy {
226            rotator.remove_old_files(keep_count)?;
227        }
228
229        Ok(rotator)
230    }
231
232    fn open_file(&mut self) -> Result<(), Error> {
233        let file = OpenOptions::new()
234            .create(true)
235            .append(true)
236            .open(&self.path)?;
237        self.current_size = file.metadata()?.len();
238        self.inner = Some(file);
239        Ok(())
240    }
241
242    fn rotate(&mut self) -> Result<(), Error> {
243        if let Some(mut file) = self.inner.take() {
244            let _ = file.flush();
245        }
246        if self.path.exists() {
247            match self.rotation_strategy {
248                RotationStrategy::KeepAll => {
249                    self.rename_file_to_dated()?;
250                }
251                RotationStrategy::KeepSome(keep_count) => {
252                    // remove_old_files excludes the active file.
253                    // So we need to keep (keep_count - 1) archived files to make room for the one we are about to archive.
254                    self.remove_old_files(keep_count - 1)?;
255                    self.rename_file_to_dated()?;
256                }
257                RotationStrategy::KeepOne => {
258                    fs::remove_file(&self.path)?;
259                }
260            }
261        }
262        self.open_file()?;
263        Ok(())
264    }
265
266    /// Remove old log files until the number of old log files is equal to the keep_count,
267    /// the current active log file is not included in the keep_count.
268    fn remove_old_files(&self, keep_count: usize) -> Result<(), Error> {
269        let mut files = fs::read_dir(&self.dir)?
270            .filter_map(|entry| {
271                let entry = entry.ok()?;
272                let path = entry.path();
273                let old_file_name = path.file_name()?.to_string_lossy().into_owned();
274                if old_file_name.starts_with(&self.file_name)
275                  // exclude the current active file
276                  && old_file_name != format!("{}.log", self.file_name)
277                {
278                    let date = old_file_name
279                        .strip_prefix(&self.file_name)?
280                        .strip_prefix("_")?
281                        .strip_suffix(".log")?;
282                    Some((path, date.to_string()))
283                } else {
284                    None
285                }
286            })
287            .collect::<Vec<_>>();
288
289        files.sort_by(|a, b| a.1.cmp(&b.1));
290
291        if files.len() > keep_count {
292            let files_to_remove = files.len() - keep_count;
293            for (old_log_path, _) in files.iter().take(files_to_remove) {
294                fs::remove_file(old_log_path)?;
295            }
296        }
297        Ok(())
298    }
299
300    fn rename_file_to_dated(&self) -> Result<(), Error> {
301        let to = self.dir.join(format!(
302            "{}_{}.log",
303            self.file_name,
304            self.timezone_strategy
305                .get_now()
306                .format(LOG_DATE_FORMAT)
307                .unwrap(),
308        ));
309        if to.is_file() {
310            // designated rotated log file name already exists
311            // highly unlikely but defensively handle anyway by adding .bak to filename
312            let mut to_bak = to.clone();
313            to_bak.set_file_name(format!(
314                "{}.bak",
315                to_bak.file_name().unwrap().to_string_lossy()
316            ));
317            fs::rename(&to, to_bak)?;
318        }
319        fs::rename(&self.path, &to)?;
320        Ok(())
321    }
322}
323
324impl Write for RotatingFile {
325    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
326        self.buffer.extend_from_slice(buf);
327        Ok(buf.len())
328    }
329
330    fn flush(&mut self) -> std::io::Result<()> {
331        if self.buffer.is_empty() {
332            return Ok(());
333        }
334        if self.inner.is_none() {
335            self.open_file().map_err(std::io::Error::other)?;
336        }
337
338        if self.current_size != 0 && self.current_size + (self.buffer.len() as u64) > self.max_size
339        {
340            self.rotate().map_err(std::io::Error::other)?;
341        }
342
343        if let Some(file) = self.inner.as_mut() {
344            file.write_all(&self.buffer)?;
345            self.current_size += self.buffer.len() as u64;
346            file.flush()?;
347        }
348        self.buffer.clear();
349        Ok(())
350    }
351}
352
353#[derive(Debug, Serialize, Clone)]
354struct RecordPayload {
355    message: String,
356    level: LogLevel,
357}
358
359/// An enum representing the available targets of the logger.
360pub enum TargetKind {
361    /// Print logs to stdout.
362    Stdout,
363    /// Print logs to stderr.
364    Stderr,
365    /// Write logs to the given directory.
366    ///
367    /// The plugin will ensure the directory exists before writing logs.
368    Folder {
369        /// Path of the directory to write the log file to.
370        path: PathBuf,
371        /// Name of the log file, without extension. Defaults to the app's package name when `None`.
372        file_name: Option<String>,
373    },
374    /// Write logs to the OS specific logs directory.
375    ///
376    /// ### Platform-specific
377    ///
378    /// |Platform   | Value                                                                                     | Example                                                     |
379    /// | --------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
380    /// | Linux     | `$XDG_DATA_HOME/{bundleIdentifier}/logs` or `$HOME/.local/share/{bundleIdentifier}/logs`  | `/home/alice/.local/share/com.tauri.dev/logs`               |
381    /// | macOS/iOS | `{homeDir}/Library/Logs/{bundleIdentifier}`                                               | `/Users/Alice/Library/Logs/com.tauri.dev`                   |
382    /// | Windows   | `{FOLDERID_LocalAppData}/{bundleIdentifier}/logs`                                         | `C:\Users\Alice\AppData\Local\com.tauri.dev\logs`           |
383    /// | Android   | `{ConfigDir}/logs`                                                                        | `/data/data/com.tauri.dev/files/logs`                       |
384    LogDir {
385        /// Name of the log file, without extension. Defaults to the app's package name when `None`.
386        file_name: Option<String>,
387    },
388    /// Forward logs to the webview (via the `log://log` event).
389    ///
390    /// This requires the webview to subscribe to log events, via this plugins `attachConsole` function.
391    Webview,
392    /// Send logs to a [`fern::Dispatch`]
393    ///
394    /// You can use this to construct arbitrary log targets.
395    Dispatch(fern::Dispatch),
396}
397
398type Formatter = dyn Fn(FormatCallback, &Arguments, &Record) + Send + Sync + 'static;
399
400/// A log target.
401pub struct Target {
402    kind: TargetKind,
403    filters: Vec<Box<Filter>>,
404    formatter: Option<Box<Formatter>>,
405}
406
407impl Target {
408    /// Creates a new [`Target`] of the given [`TargetKind`], with no filters and the [`Builder`]'s formatter.
409    #[inline]
410    pub const fn new(kind: TargetKind) -> Self {
411        Self {
412            kind,
413            filters: Vec::new(),
414            formatter: None,
415        }
416    }
417
418    /// Adds a filter that can reject log records from being sent to this target. Multiple filters may be added by
419    /// calling this method more than once.
420    #[inline]
421    pub fn filter<F>(mut self, filter: F) -> Self
422    where
423        F: Fn(&log::Metadata) -> bool + Send + Sync + 'static,
424    {
425        self.filters.push(Box::new(filter));
426        self
427    }
428
429    /// Sets a formatter for log records sent to this target, overriding the [`Builder`]'s formatter for this
430    /// target only.
431    #[inline]
432    pub fn format<F>(mut self, formatter: F) -> Self
433    where
434        F: Fn(FormatCallback, &Arguments, &Record) + Send + Sync + 'static,
435    {
436        self.formatter.replace(Box::new(formatter));
437        self
438    }
439}
440
441/// Builds the log plugin, configuring targets, rotation, formatting and level filters before attaching the
442/// resulting logger as the global logger for the [`log`] crate.
443pub struct Builder {
444    dispatch: fern::Dispatch,
445    rotation_strategy: RotationStrategy,
446    timezone_strategy: TimezoneStrategy,
447    file_open_strategy: FileOpenStrategy,
448    max_file_size: u128,
449    targets: Vec<Target>,
450    is_skip_logger: bool,
451}
452
453impl Default for Builder {
454    fn default() -> Self {
455        #[cfg(desktop)]
456        let format = format_description!("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]");
457        let dispatch = fern::Dispatch::new().format(move |out, message, record| {
458            out.finish(
459                #[cfg(mobile)]
460                format_args!("[{}] {}", record.target(), message),
461                #[cfg(desktop)]
462                format_args!(
463                    "{}[{}][{}] {}",
464                    DEFAULT_TIMEZONE_STRATEGY.get_now().format(&format).unwrap(),
465                    record.target(),
466                    record.level(),
467                    message
468                ),
469            )
470        });
471        Self {
472            dispatch,
473            rotation_strategy: DEFAULT_ROTATION_STRATEGY,
474            timezone_strategy: DEFAULT_TIMEZONE_STRATEGY,
475            file_open_strategy: DEFAULT_FILE_OPEN_STRATEGY,
476            max_file_size: DEFAULT_MAX_FILE_SIZE as u128,
477            targets: DEFAULT_LOG_TARGETS.into(),
478            is_skip_logger: false,
479        }
480    }
481}
482
483impl Builder {
484    /// Creates a new [`Builder`] with the default configuration: targets [`TargetKind::Stdout`] and
485    /// [`TargetKind::LogDir`] (using the app's package name as the file name), [`RotationStrategy::KeepOne`],
486    /// [`TimezoneStrategy::UseUtc`], [`FileOpenStrategy::Append`], a maximum log file size of `40_000` bytes
487    /// (see [`Self::max_file_size`]) and the default `fern` formatter.
488    pub fn new() -> Self {
489        Default::default()
490    }
491
492    /// Sets the [`RotationStrategy`].
493    ///
494    /// Default is [`RotationStrategy::KeepOne`]
495    pub fn rotation_strategy(mut self, rotation_strategy: RotationStrategy) -> Self {
496        self.rotation_strategy = rotation_strategy;
497        self
498    }
499
500    /// Sets the [`TimezoneStrategy`].
501    /// Calling this method overrides the format set in [`Self::format`].
502    ///
503    /// Default is [`TimezoneStrategy::UseUtc`]
504    pub fn timezone_strategy(mut self, timezone_strategy: TimezoneStrategy) -> Self {
505        self.timezone_strategy = timezone_strategy.clone();
506
507        let format = format_description!("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]");
508        self.dispatch = self.dispatch.format(move |out, message, record| {
509            out.finish(format_args!(
510                "{}[{}][{}] {}",
511                timezone_strategy.get_now().format(&format).unwrap(),
512                record.level(),
513                record.target(),
514                message
515            ))
516        });
517        self
518    }
519
520    /// Sets the strategy to open the log file.
521    ///
522    /// The default is [`FileOpenStrategy::Append`].
523    pub fn file_open_strategy(mut self, file_open_strategy: FileOpenStrategy) -> Self {
524        self.file_open_strategy = file_open_strategy;
525        self
526    }
527
528    /// Sets the maximum file size in bytes for log rotation.
529    ///
530    /// Values larger than [`u64::MAX`] will be clamped to [`u64::MAX`].
531    /// In v3, this parameter will be changed to `u64`.
532    ///
533    /// Default is `40_000`
534    pub fn max_file_size(mut self, max_file_size: u128) -> Self {
535        self.max_file_size = max_file_size.min(u64::MAX as u128);
536        self
537    }
538
539    /// Clears the format so that only the message is logged.
540    ///
541    /// e.g. `log::info!("message")` will log out `message`
542    pub fn clear_format(mut self) -> Self {
543        self.dispatch = self.dispatch.format(|out, message, _record| {
544            out.finish(format_args!("{message}"));
545        });
546        self
547    }
548
549    /// Sets the formatter of this dispatch. The closure should accept a
550    /// callback, a message and a log record, and write the resulting
551    /// format to the writer.
552    ///
553    /// The log record is passed for completeness, but the `args()` method of
554    /// the record should be ignored, and the [`std::fmt::Arguments`] given
555    /// should be used instead. `record.args()` may be used to retrieve the
556    /// _original_ log message, but in order to allow for true log
557    /// chaining, formatters should use the given message instead whenever
558    /// including the message in the output.
559    ///
560    /// To avoid all allocation of intermediate results, the formatter is
561    /// "completed" by calling a callback, which then calls the rest of the
562    /// logging chain with the new formatted message. The callback object keeps
563    /// track of if it was called or not via a stack boolean as well, so if
564    /// you don't use `out.finish` the log message will continue down
565    /// the logger chain unformatted.
566    ///
567    /// Example usage:
568    ///
569    /// ```
570    /// tauri_plugin_log::Builder::new()
571    ///     .format(|out, message, record| {
572    ///         out.finish(format_args!(
573    ///             "[{} {}] {}",
574    ///             record.level(),
575    ///             record.target(),
576    ///             message
577    ///         ))
578    ///     });
579    /// ```
580    pub fn format<F>(mut self, formatter: F) -> Self
581    where
582        F: Fn(FormatCallback, &Arguments, &Record) + Sync + Send + 'static,
583    {
584        self.dispatch = self.dispatch.format(formatter);
585        self
586    }
587
588    /// Sets the overarching level filter for this logger.
589    /// All messages not already filtered by something set by [`Self::level_for`] will be affected.
590    ///
591    /// All messages filtered will be discarded if less severe than the given level.
592    ///
593    /// Default level is [`log::LevelFilter::Trace`].
594    pub fn level(mut self, level_filter: impl Into<LevelFilter>) -> Self {
595        self.dispatch = self.dispatch.level(level_filter.into());
596        self
597    }
598
599    /// Sets a per-target log level filter. Default target for log messages is
600    /// `crate_name::module_name` or
601    /// `crate_name` for logs in the crate root. Targets can also be set with
602    /// `info!(target: "target-name", ...)`.
603    ///
604    /// For each log record fern will first try to match the most specific
605    /// level_for, and then progressively more general ones until either a
606    /// matching level is found, or the default level is used.
607    ///
608    /// For example, a log for the target `hyper::http::h1` will first test a
609    /// level_for for `hyper::http::h1`, then for `hyper::http`, then for
610    /// `hyper`, then use the default level.
611    ///
612    /// Examples:
613    ///
614    /// A program wants to include a lot of debugging output, but the library
615    /// "hyper" is known to work well, so debug output from it should be
616    /// excluded:
617    ///
618    /// ```
619    /// # fn main() {
620    /// tauri_plugin_log::Builder::new()
621    ///     .level(log::LevelFilter::Trace)
622    ///     .level_for("hyper", log::LevelFilter::Info)
623    ///     # ;
624    /// # }
625    /// ```
626    pub fn level_for(mut self, module: impl Into<Cow<'static, str>>, level: LevelFilter) -> Self {
627        self.dispatch = self.dispatch.level_for(module, level);
628        self
629    }
630
631    /// Adds a custom filter which can reject messages passing through this logger.
632    ///
633    /// [`Self::level`] and [`Self::level_for`] are preferred if applicable.
634    ///
635    /// Example usage:
636    ///
637    /// ```
638    /// tauri_plugin_log::Builder::new()
639    ///     .level(log::LevelFilter::Info)
640    ///     .filter(|metadata| {
641    ///         // Reject messages with the `Error` log level.
642    ///         metadata.level() != log::Level::Error
643    ///     });
644    /// ```
645    pub fn filter<F>(mut self, filter: F) -> Self
646    where
647        F: Fn(&log::Metadata) -> bool + Send + Sync + 'static,
648    {
649        self.dispatch = self.dispatch.filter(filter);
650        self
651    }
652
653    /// Removes all targets. Useful to ignore the default targets and reconfigure them.
654    pub fn clear_targets(mut self) -> Self {
655        self.targets.clear();
656        self
657    }
658
659    /// Adds a log target to the logger.
660    ///
661    /// ```rust
662    /// use tauri_plugin_log::{Target, TargetKind};
663    /// tauri_plugin_log::Builder::new()
664    ///     .target(Target::new(TargetKind::Webview));
665    /// ```
666    ///
667    /// The default targets are
668    ///
669    /// ```rust
670    /// # use tauri_plugin_log::{Target, TargetKind, Builder};
671    /// # Builder::new()
672    /// #     .targets(
673    /// [
674    ///     Target::new(TargetKind::Stdout),
675    ///     Target::new(TargetKind::LogDir { file_name: None }),
676    /// ]
677    /// #      );
678    /// ```
679    pub fn target(mut self, target: Target) -> Self {
680        self.targets.push(target);
681        self
682    }
683
684    /// Skip the creation and global registration of a logger
685    ///
686    /// 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.
687    /// In this configuration, no logger will be created and the plugin's `log` command will rely on the result of `log::logger()`.
688    /// You will be responsible for configuring the logger yourself and any included targets will be ignored.
689    /// 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.
690    /// 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.
691    /// Both scenarios require calling this method.
692    ///
693    /// ```rust
694    /// # struct SimpleLogger;
695    /// # impl log::Log for SimpleLogger {
696    /// #     fn enabled(&self, _metadata: &log::Metadata) -> bool { true }
697    /// #     fn log(&self, _record: &log::Record) {}
698    /// #     fn flush(&self) {}
699    /// # }
700    /// static LOGGER: SimpleLogger = SimpleLogger;
701    ///
702    /// log::set_logger(&LOGGER).expect("a logger was already set");
703    /// log::set_max_level(log::LevelFilter::Info);
704    /// tauri_plugin_log::Builder::new()
705    ///     .skip_logger();
706    /// ```
707    pub fn skip_logger(mut self) -> Self {
708        self.is_skip_logger = true;
709        self
710    }
711
712    /// Replaces the targets of the logger.
713    ///
714    /// ```rust
715    /// use tauri_plugin_log::{Target, TargetKind, WEBVIEW_TARGET};
716    /// tauri_plugin_log::Builder::new()
717    ///     .targets([
718    ///         Target::new(TargetKind::Webview),
719    ///         Target::new(TargetKind::LogDir { file_name: Some("webview".into()) }).filter(|metadata| metadata.target().starts_with(WEBVIEW_TARGET)),
720    ///         Target::new(TargetKind::LogDir { file_name: Some("rust".into()) }).filter(|metadata| !metadata.target().starts_with(WEBVIEW_TARGET)),
721    ///     ]);
722    /// ```
723    ///
724    /// The default targets are
725    ///
726    /// ```rust
727    /// # use tauri_plugin_log::{Target, TargetKind, Builder};
728    /// # Builder::new()
729    /// #     .targets(
730    /// [
731    ///     Target::new(TargetKind::Stdout),
732    ///     Target::new(TargetKind::LogDir { file_name: None }),
733    /// ]
734    /// #      );
735    /// ```
736    pub fn targets(mut self, targets: impl IntoIterator<Item = Target>) -> Self {
737        self.targets = Vec::from_iter(targets);
738        self
739    }
740
741    #[cfg(feature = "colored")]
742    pub fn with_colors(self, colors: fern::colors::ColoredLevelConfig) -> Self {
743        let format = format_description!("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]");
744
745        let timezone_strategy = self.timezone_strategy.clone();
746        self.format(move |out, message, record| {
747            out.finish(format_args!(
748                "{}[{}][{}] {}",
749                timezone_strategy.get_now().format(&format).unwrap(),
750                colors.color(record.level()),
751                record.target(),
752                message
753            ))
754        })
755    }
756
757    fn acquire_logger<R: Runtime>(
758        app_handle: &AppHandle<R>,
759        mut dispatch: fern::Dispatch,
760        rotation_strategy: RotationStrategy,
761        timezone_strategy: TimezoneStrategy,
762        file_open_strategy: FileOpenStrategy,
763        max_file_size: u64,
764        targets: Vec<Target>,
765    ) -> Result<(log::LevelFilter, Box<dyn log::Log>), Error> {
766        let app_name = &app_handle.package_info().name;
767
768        // setup targets
769        for target in targets {
770            let mut target_dispatch = fern::Dispatch::new();
771            for filter in target.filters {
772                target_dispatch = target_dispatch.filter(filter);
773            }
774            if let Some(formatter) = target.formatter {
775                target_dispatch = target_dispatch.format(formatter);
776            }
777
778            let logger = match target.kind {
779                #[cfg(target_os = "android")]
780                TargetKind::Stdout | TargetKind::Stderr => fern::Output::call(android_logger::log),
781                #[cfg(target_os = "ios")]
782                TargetKind::Stdout | TargetKind::Stderr => fern::Output::call(move |record| {
783                    let message = format!("{}", record.args());
784                    unsafe {
785                        ios::tauri_log(
786                            match record.level() {
787                                log::Level::Trace | log::Level::Debug => 1,
788                                log::Level::Info => 2,
789                                log::Level::Warn | log::Level::Error => 3,
790                            },
791                            // The string is allocated in rust, so we must
792                            // autorelease it rust to give it to the Swift
793                            // runtime.
794                            objc2::rc::Retained::autorelease_ptr(
795                                objc2_foundation::NSString::from_str(message.as_str()),
796                            ) as _,
797                        );
798                    }
799                }),
800                #[cfg(desktop)]
801                TargetKind::Stdout => std::io::stdout().into(),
802                #[cfg(desktop)]
803                TargetKind::Stderr => std::io::stderr().into(),
804                TargetKind::Folder { path, file_name } => {
805                    if !path.exists() {
806                        fs::create_dir_all(&path)?;
807                    }
808
809                    let rotator = RotatingFile::new(
810                        &path,
811                        file_name.unwrap_or(app_name.clone()),
812                        max_file_size,
813                        rotation_strategy.clone(),
814                        timezone_strategy.clone(),
815                        file_open_strategy.clone(),
816                    )?;
817                    fern::Output::writer(Box::new(rotator), "\n")
818                }
819                TargetKind::LogDir { file_name } => {
820                    let path = app_handle.path().app_log_dir()?;
821                    if !path.exists() {
822                        fs::create_dir_all(&path)?;
823                    }
824
825                    let rotator = RotatingFile::new(
826                        &path,
827                        file_name.unwrap_or(app_name.clone()),
828                        max_file_size,
829                        rotation_strategy.clone(),
830                        timezone_strategy.clone(),
831                        file_open_strategy.clone(),
832                    )?;
833                    fern::Output::writer(Box::new(rotator), "\n")
834                }
835                TargetKind::Webview => {
836                    let app_handle = app_handle.clone();
837
838                    fern::Output::call(move |record| {
839                        let payload = RecordPayload {
840                            message: record.args().to_string(),
841                            level: record.level().into(),
842                        };
843                        let app_handle = app_handle.clone();
844                        tauri::async_runtime::spawn(async move {
845                            let _ = app_handle.emit("log://log", payload);
846                        });
847                    })
848                }
849                TargetKind::Dispatch(dispatch) => dispatch.into(),
850            };
851            target_dispatch = target_dispatch.chain(logger);
852
853            dispatch = dispatch.chain(target_dispatch);
854        }
855
856        Ok(dispatch.into_log())
857    }
858
859    fn plugin_builder<R: Runtime>() -> plugin::Builder<R> {
860        plugin::Builder::new("log").invoke_handler(tauri::generate_handler![commands::log])
861    }
862
863    /// Builds the [`TauriPlugin`] and returns it together with the configured [`log::LevelFilter`] and [`log::Log`]
864    /// implementation, instead of registering them as the global logger.
865    ///
866    /// Use this instead of [`Self::build`] when this logger needs to be combined with another one, e.g. when
867    /// forwarding logs to `tracing` via `tracing-log`, since only a single global logger can be installed.
868    ///
869    /// # Errors
870    ///
871    /// Returns [`Error::LoggerNotInitialized`] if [`Self::skip_logger`] was called on this builder.
872    #[allow(clippy::type_complexity)]
873    pub fn split<R: Runtime>(
874        self,
875        app_handle: &AppHandle<R>,
876    ) -> Result<(TauriPlugin<R>, log::LevelFilter, Box<dyn log::Log>), Error> {
877        if self.is_skip_logger {
878            return Err(Error::LoggerNotInitialized);
879        }
880        let plugin = Self::plugin_builder();
881        let (max_level, log) = Self::acquire_logger(
882            app_handle,
883            self.dispatch,
884            self.rotation_strategy,
885            self.timezone_strategy,
886            self.file_open_strategy,
887            self.max_file_size as u64,
888            self.targets,
889        )?;
890
891        Ok((plugin.build(), max_level, log))
892    }
893
894    /// Builds the [`TauriPlugin`], attaching the configured logger as the global logger for the [`log`] crate on
895    /// setup, unless [`Self::skip_logger`] was called on this builder.
896    pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
897        Self::plugin_builder()
898            .setup(move |app_handle, _api| {
899                if !self.is_skip_logger {
900                    let (max_level, log) = Self::acquire_logger(
901                        app_handle,
902                        self.dispatch,
903                        self.rotation_strategy,
904                        self.timezone_strategy,
905                        self.file_open_strategy,
906                        self.max_file_size as u64,
907                        self.targets,
908                    )?;
909                    attach_logger(max_level, log)?;
910                }
911                Ok(())
912            })
913            .build()
914    }
915}
916
917/// Attaches the given logger
918pub fn attach_logger(
919    max_level: log::LevelFilter,
920    log: Box<dyn log::Log>,
921) -> Result<(), log::SetLoggerError> {
922    log::set_boxed_logger(log)?;
923    log::set_max_level(max_level);
924    Ok(())
925}