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: u64,
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,
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 /// Default is `40_000`
531 pub fn max_file_size(mut self, max_file_size: u64) -> Self {
532 self.max_file_size = max_file_size;
533 self
534 }
535
536 /// Clears the format so that only the message is logged.
537 ///
538 /// e.g. `log::info!("message")` will log out `message`
539 pub fn clear_format(mut self) -> Self {
540 self.dispatch = self.dispatch.format(|out, message, _record| {
541 out.finish(format_args!("{message}"));
542 });
543 self
544 }
545
546 /// Sets the formatter of this dispatch. The closure should accept a
547 /// callback, a message and a log record, and write the resulting
548 /// format to the writer.
549 ///
550 /// The log record is passed for completeness, but the `args()` method of
551 /// the record should be ignored, and the [`std::fmt::Arguments`] given
552 /// should be used instead. `record.args()` may be used to retrieve the
553 /// _original_ log message, but in order to allow for true log
554 /// chaining, formatters should use the given message instead whenever
555 /// including the message in the output.
556 ///
557 /// To avoid all allocation of intermediate results, the formatter is
558 /// "completed" by calling a callback, which then calls the rest of the
559 /// logging chain with the new formatted message. The callback object keeps
560 /// track of if it was called or not via a stack boolean as well, so if
561 /// you don't use `out.finish` the log message will continue down
562 /// the logger chain unformatted.
563 ///
564 /// Example usage:
565 ///
566 /// ```
567 /// tauri_plugin_log::Builder::new()
568 /// .format(|out, message, record| {
569 /// out.finish(format_args!(
570 /// "[{} {}] {}",
571 /// record.level(),
572 /// record.target(),
573 /// message
574 /// ))
575 /// });
576 /// ```
577 pub fn format<F>(mut self, formatter: F) -> Self
578 where
579 F: Fn(FormatCallback, &Arguments, &Record) + Sync + Send + 'static,
580 {
581 self.dispatch = self.dispatch.format(formatter);
582 self
583 }
584
585 /// Sets the overarching level filter for this logger.
586 /// All messages not already filtered by something set by [`Self::level_for`] will be affected.
587 ///
588 /// All messages filtered will be discarded if less severe than the given level.
589 ///
590 /// Default level is [`log::LevelFilter::Trace`].
591 pub fn level(mut self, level_filter: impl Into<LevelFilter>) -> Self {
592 self.dispatch = self.dispatch.level(level_filter.into());
593 self
594 }
595
596 /// Sets a per-target log level filter. Default target for log messages is
597 /// `crate_name::module_name` or
598 /// `crate_name` for logs in the crate root. Targets can also be set with
599 /// `info!(target: "target-name", ...)`.
600 ///
601 /// For each log record fern will first try to match the most specific
602 /// level_for, and then progressively more general ones until either a
603 /// matching level is found, or the default level is used.
604 ///
605 /// For example, a log for the target `hyper::http::h1` will first test a
606 /// level_for for `hyper::http::h1`, then for `hyper::http`, then for
607 /// `hyper`, then use the default level.
608 ///
609 /// Examples:
610 ///
611 /// A program wants to include a lot of debugging output, but the library
612 /// "hyper" is known to work well, so debug output from it should be
613 /// excluded:
614 ///
615 /// ```
616 /// # fn main() {
617 /// tauri_plugin_log::Builder::new()
618 /// .level(log::LevelFilter::Trace)
619 /// .level_for("hyper", log::LevelFilter::Info)
620 /// # ;
621 /// # }
622 /// ```
623 pub fn level_for(mut self, module: impl Into<Cow<'static, str>>, level: LevelFilter) -> Self {
624 self.dispatch = self.dispatch.level_for(module, level);
625 self
626 }
627
628 /// Adds a custom filter which can reject messages passing through this logger.
629 ///
630 /// [`Self::level`] and [`Self::level_for`] are preferred if applicable.
631 ///
632 /// Example usage:
633 ///
634 /// ```
635 /// tauri_plugin_log::Builder::new()
636 /// .level(log::LevelFilter::Info)
637 /// .filter(|metadata| {
638 /// // Reject messages with the `Error` log level.
639 /// metadata.level() != log::Level::Error
640 /// });
641 /// ```
642 pub fn filter<F>(mut self, filter: F) -> Self
643 where
644 F: Fn(&log::Metadata) -> bool + Send + Sync + 'static,
645 {
646 self.dispatch = self.dispatch.filter(filter);
647 self
648 }
649
650 /// Removes all targets. Useful to ignore the default targets and reconfigure them.
651 pub fn clear_targets(mut self) -> Self {
652 self.targets.clear();
653 self
654 }
655
656 /// Adds a log target to the logger.
657 ///
658 /// ```rust
659 /// use tauri_plugin_log::{Target, TargetKind};
660 /// tauri_plugin_log::Builder::new()
661 /// .target(Target::new(TargetKind::Webview));
662 /// ```
663 ///
664 /// The default targets are
665 ///
666 /// ```rust
667 /// # use tauri_plugin_log::{Target, TargetKind, Builder};
668 /// # Builder::new()
669 /// # .targets(
670 /// [
671 /// Target::new(TargetKind::Stdout),
672 /// Target::new(TargetKind::LogDir { file_name: None }),
673 /// ]
674 /// # );
675 /// ```
676 pub fn target(mut self, target: Target) -> Self {
677 self.targets.push(target);
678 self
679 }
680
681 /// Skip the creation and global registration of a logger
682 ///
683 /// 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.
684 /// In this configuration, no logger will be created and the plugin's `log` command will rely on the result of `log::logger()`.
685 /// You will be responsible for configuring the logger yourself and any included targets will be ignored.
686 /// 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.
687 /// 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.
688 /// Both scenarios require calling this method.
689 ///
690 /// ```rust
691 /// # struct SimpleLogger;
692 /// # impl log::Log for SimpleLogger {
693 /// # fn enabled(&self, _metadata: &log::Metadata) -> bool { true }
694 /// # fn log(&self, _record: &log::Record) {}
695 /// # fn flush(&self) {}
696 /// # }
697 /// static LOGGER: SimpleLogger = SimpleLogger;
698 ///
699 /// log::set_logger(&LOGGER).expect("a logger was already set");
700 /// log::set_max_level(log::LevelFilter::Info);
701 /// tauri_plugin_log::Builder::new()
702 /// .skip_logger();
703 /// ```
704 pub fn skip_logger(mut self) -> Self {
705 self.is_skip_logger = true;
706 self
707 }
708
709 /// Replaces the targets of the logger.
710 ///
711 /// ```rust
712 /// use tauri_plugin_log::{Target, TargetKind, WEBVIEW_TARGET};
713 /// tauri_plugin_log::Builder::new()
714 /// .targets([
715 /// Target::new(TargetKind::Webview),
716 /// Target::new(TargetKind::LogDir { file_name: Some("webview".into()) }).filter(|metadata| metadata.target().starts_with(WEBVIEW_TARGET)),
717 /// Target::new(TargetKind::LogDir { file_name: Some("rust".into()) }).filter(|metadata| !metadata.target().starts_with(WEBVIEW_TARGET)),
718 /// ]);
719 /// ```
720 ///
721 /// The default targets are
722 ///
723 /// ```rust
724 /// # use tauri_plugin_log::{Target, TargetKind, Builder};
725 /// # Builder::new()
726 /// # .targets(
727 /// [
728 /// Target::new(TargetKind::Stdout),
729 /// Target::new(TargetKind::LogDir { file_name: None }),
730 /// ]
731 /// # );
732 /// ```
733 pub fn targets(mut self, targets: impl IntoIterator<Item = Target>) -> Self {
734 self.targets = Vec::from_iter(targets);
735 self
736 }
737
738 #[cfg(feature = "colored")]
739 pub fn with_colors(self, colors: fern::colors::ColoredLevelConfig) -> Self {
740 let format = format_description!("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]");
741
742 let timezone_strategy = self.timezone_strategy.clone();
743 self.format(move |out, message, record| {
744 out.finish(format_args!(
745 "{}[{}][{}] {}",
746 timezone_strategy.get_now().format(&format).unwrap(),
747 colors.color(record.level()),
748 record.target(),
749 message
750 ))
751 })
752 }
753
754 fn acquire_logger<R: Runtime>(
755 app_handle: &AppHandle<R>,
756 mut dispatch: fern::Dispatch,
757 rotation_strategy: RotationStrategy,
758 timezone_strategy: TimezoneStrategy,
759 file_open_strategy: FileOpenStrategy,
760 max_file_size: u64,
761 targets: Vec<Target>,
762 ) -> Result<(log::LevelFilter, Box<dyn log::Log>), Error> {
763 let app_name = &app_handle.package_info().name;
764
765 // setup targets
766 for target in targets {
767 let mut target_dispatch = fern::Dispatch::new();
768 for filter in target.filters {
769 target_dispatch = target_dispatch.filter(filter);
770 }
771 if let Some(formatter) = target.formatter {
772 target_dispatch = target_dispatch.format(formatter);
773 }
774
775 let logger = match target.kind {
776 #[cfg(target_os = "android")]
777 TargetKind::Stdout | TargetKind::Stderr => fern::Output::call(android_logger::log),
778 #[cfg(target_os = "ios")]
779 TargetKind::Stdout | TargetKind::Stderr => fern::Output::call(move |record| {
780 let message = format!("{}", record.args());
781 unsafe {
782 ios::tauri_log(
783 match record.level() {
784 log::Level::Trace | log::Level::Debug => 1,
785 log::Level::Info => 2,
786 log::Level::Warn | log::Level::Error => 3,
787 },
788 // The string is allocated in rust, so we must
789 // autorelease it rust to give it to the Swift
790 // runtime.
791 objc2::rc::Retained::autorelease_ptr(
792 objc2_foundation::NSString::from_str(message.as_str()),
793 ) as _,
794 );
795 }
796 }),
797 #[cfg(desktop)]
798 TargetKind::Stdout => std::io::stdout().into(),
799 #[cfg(desktop)]
800 TargetKind::Stderr => std::io::stderr().into(),
801 TargetKind::Folder { path, file_name } => {
802 if !path.exists() {
803 fs::create_dir_all(&path)?;
804 }
805
806 let rotator = RotatingFile::new(
807 &path,
808 file_name.unwrap_or(app_name.clone()),
809 max_file_size,
810 rotation_strategy.clone(),
811 timezone_strategy.clone(),
812 file_open_strategy.clone(),
813 )?;
814 fern::Output::writer(Box::new(rotator), "\n")
815 }
816 TargetKind::LogDir { file_name } => {
817 let path = app_handle.path().app_log_dir()?;
818 if !path.exists() {
819 fs::create_dir_all(&path)?;
820 }
821
822 let rotator = RotatingFile::new(
823 &path,
824 file_name.unwrap_or(app_name.clone()),
825 max_file_size,
826 rotation_strategy.clone(),
827 timezone_strategy.clone(),
828 file_open_strategy.clone(),
829 )?;
830 fern::Output::writer(Box::new(rotator), "\n")
831 }
832 TargetKind::Webview => {
833 let app_handle = app_handle.clone();
834
835 fern::Output::call(move |record| {
836 let payload = RecordPayload {
837 message: record.args().to_string(),
838 level: record.level().into(),
839 };
840 let app_handle = app_handle.clone();
841 tauri::async_runtime::spawn(async move {
842 let _ = app_handle.emit("log://log", payload);
843 });
844 })
845 }
846 TargetKind::Dispatch(dispatch) => dispatch.into(),
847 };
848 target_dispatch = target_dispatch.chain(logger);
849
850 dispatch = dispatch.chain(target_dispatch);
851 }
852
853 Ok(dispatch.into_log())
854 }
855
856 fn plugin_builder<R: Runtime>() -> plugin::Builder<R> {
857 plugin::Builder::new("log").invoke_handler(tauri::generate_handler![commands::log])
858 }
859
860 /// Builds the [`TauriPlugin`] and returns it together with the configured [`log::LevelFilter`] and [`log::Log`]
861 /// implementation, instead of registering them as the global logger.
862 ///
863 /// Use this instead of [`Self::build`] when this logger needs to be combined with another one, e.g. when
864 /// forwarding logs to `tracing` via `tracing-log`, since only a single global logger can be installed.
865 ///
866 /// # Errors
867 ///
868 /// Returns [`Error::LoggerNotInitialized`] if [`Self::skip_logger`] was called on this builder.
869 #[allow(clippy::type_complexity)]
870 pub fn split<R: Runtime>(
871 self,
872 app_handle: &AppHandle<R>,
873 ) -> Result<(TauriPlugin<R>, log::LevelFilter, Box<dyn log::Log>), Error> {
874 if self.is_skip_logger {
875 return Err(Error::LoggerNotInitialized);
876 }
877 let plugin = Self::plugin_builder();
878 let (max_level, log) = Self::acquire_logger(
879 app_handle,
880 self.dispatch,
881 self.rotation_strategy,
882 self.timezone_strategy,
883 self.file_open_strategy,
884 self.max_file_size,
885 self.targets,
886 )?;
887
888 Ok((plugin.build(), max_level, log))
889 }
890
891 /// Builds the [`TauriPlugin`], attaching the configured logger as the global logger for the [`log`] crate on
892 /// setup, unless [`Self::skip_logger`] was called on this builder.
893 pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
894 Self::plugin_builder()
895 .setup(move |app_handle, _api| {
896 if !self.is_skip_logger {
897 let (max_level, log) = Self::acquire_logger(
898 app_handle,
899 self.dispatch,
900 self.rotation_strategy,
901 self.timezone_strategy,
902 self.file_open_strategy,
903 self.max_file_size,
904 self.targets,
905 )?;
906 attach_logger(max_level, log)?;
907 }
908 Ok(())
909 })
910 .build()
911 }
912}
913
914/// Attaches the given logger
915pub fn attach_logger(
916 max_level: log::LevelFilter,
917 log: Box<dyn log::Log>,
918) -> Result<(), log::SetLoggerError> {
919 log::set_boxed_logger(log)?;
920 log::set_max_level(max_level);
921 Ok(())
922}