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