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