tauri_plugin_tracing/lib.rs
1// Doc examples show complete Tauri programs with fn main() for clarity
2#![allow(clippy::needless_doctest_main)]
3
4//! # Tauri Plugin Tracing
5//!
6//! A Tauri plugin that integrates the [`tracing`] crate for structured logging
7//! in Tauri applications. This plugin bridges logging between the Rust backend
8//! and JavaScript frontend, providing call stack information.
9//!
10//! ## Features
11//!
12//! - **`colored`**: Enables colored terminal output using ANSI escape codes
13//! - **`specta`**: Enables TypeScript type generation via the `specta` crate
14//! - **`flamegraph`**: Enables flamegraph/flamechart profiling support
15//!
16//! ## Usage
17//!
18//! By default, this plugin does **not** set up a global tracing subscriber,
19//! following the convention that libraries should not set globals. You compose
20//! your own subscriber using [`WebviewLayer`] to forward logs to the frontend:
21//!
22//! ```rust,no_run
23//! # use tauri_plugin_tracing::{Builder, WebviewLayer, LevelFilter};
24//! # use tracing_subscriber::{Registry, layer::SubscriberExt, util::SubscriberInitExt, fmt};
25//! let tracing_builder = Builder::new()
26//! .with_max_level(LevelFilter::DEBUG)
27//! .with_target("hyper", LevelFilter::WARN);
28//! let filter = tracing_builder.build_filter();
29//!
30//! tauri::Builder::default()
31//! .plugin(tracing_builder.build())
32//! .setup(move |app| {
33//! Registry::default()
34//! .with(fmt::layer())
35//! .with(WebviewLayer::new(app.handle().clone()))
36//! .with(filter)
37//! .init();
38//! Ok(())
39//! });
40//! // .run(tauri::generate_context!("examples/default-subscriber/src-tauri/tauri.conf.json"))
41//! ```
42//!
43//! ## Quick Start
44//!
45//! For simple applications, use [`Builder::with_default_subscriber()`] to let
46//! the plugin handle all tracing setup:
47//!
48//! ```rust,no_run
49//! # use tauri_plugin_tracing::{Builder, LevelFilter};
50//! tauri::Builder::default()
51//! .plugin(
52//! Builder::new()
53//! .with_max_level(LevelFilter::DEBUG)
54//! .with_default_subscriber() // Let plugin set up tracing
55//! .build(),
56//! );
57//! // .run(tauri::generate_context!("examples/default-subscriber/src-tauri/tauri.conf.json"))
58//! ```
59//!
60//! ## File Logging
61//!
62//! For simple file logging, use [`Builder::with_file_logging()`]:
63//!
64//! ```rust,no_run
65//! # use tauri_plugin_tracing::{Builder, LevelFilter};
66//! Builder::new()
67//! .with_max_level(LevelFilter::DEBUG)
68//! .with_file_logging()
69//! .with_default_subscriber()
70//! .build::<tauri::Wry>();
71//! ```
72//!
73//! For custom subscribers, use [`tracing_appender`] directly (re-exported by this crate):
74//!
75//! ```rust,no_run
76//! # use tauri::Manager;
77//! # use tauri_plugin_tracing::{Builder, WebviewLayer, LevelFilter, tracing_appender};
78//! # use tracing_subscriber::{Registry, layer::SubscriberExt, util::SubscriberInitExt, fmt};
79//! let tracing_builder = Builder::new().with_max_level(LevelFilter::DEBUG);
80//! let filter = tracing_builder.build_filter();
81//!
82//! tauri::Builder::default()
83//! .plugin(tracing_builder.build())
84//! .setup(move |app| {
85//! let log_dir = app.path().app_log_dir()?;
86//! let file_appender = tracing_appender::rolling::daily(&log_dir, "app");
87//! let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
88//! // Store _guard in Tauri state to keep file logging active
89//!
90//! Registry::default()
91//! .with(fmt::layer())
92//! .with(fmt::layer().with_ansi(false).with_writer(non_blocking))
93//! .with(WebviewLayer::new(app.handle().clone()))
94//! .with(filter)
95//! .init();
96//! Ok(())
97//! });
98//! // .run(tauri::generate_context!("examples/default-subscriber/src-tauri/tauri.conf.json"))
99//! ```
100//!
101//! Log files rotate daily and are written to:
102//! - **macOS**: `~/Library/Logs/{bundle_identifier}/app.YYYY-MM-DD.log`
103//! - **Linux**: `~/.local/share/{bundle_identifier}/logs/app.YYYY-MM-DD.log`
104//! - **Windows**: `%LOCALAPPDATA%/{bundle_identifier}/logs/app.YYYY-MM-DD.log`
105//!
106//! ## Early Initialization
107//!
108//! For maximum control, initialize tracing before creating the Tauri app. This
109//! pattern uses [`tracing_subscriber::registry()`] with [`init()`](tracing_subscriber::util::SubscriberInitExt::init)
110//! and passes a minimal [`Builder`] to the plugin:
111//!
112//! ```rust,no_run
113//! use tauri_plugin_tracing::{Builder, StripAnsiWriter, tracing_appender};
114//! use tracing::Level;
115//! use tracing_subscriber::filter::Targets;
116//! use tracing_subscriber::layer::SubscriberExt;
117//! use tracing_subscriber::util::SubscriberInitExt;
118//! use tracing_subscriber::{fmt, registry};
119//!
120//! fn setup_logger() -> Builder {
121//! let log_dir = std::env::temp_dir().join("my-app");
122//! let _ = std::fs::create_dir_all(&log_dir);
123//!
124//! let file_appender = tracing_appender::rolling::daily(&log_dir, "app");
125//! let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
126//! std::mem::forget(guard); // Keep file logging active for app lifetime
127//!
128//! let targets = Targets::new()
129//! .with_default(Level::DEBUG)
130//! .with_target("hyper", Level::WARN)
131//! .with_target("reqwest", Level::WARN);
132//!
133//! registry()
134//! .with(fmt::layer().with_ansi(true))
135//! .with(fmt::layer().with_writer(StripAnsiWriter::new(non_blocking)).with_ansi(false))
136//! .with(targets)
137//! .init();
138//!
139//! // Return minimal builder - logging is already configured
140//! Builder::new()
141//! }
142//!
143//! fn main() {
144//! let builder = setup_logger();
145//! tauri::Builder::default()
146//! .plugin(builder.build());
147//! // .run(tauri::generate_context!("examples/default-subscriber/src-tauri/tauri.conf.json"))
148//! }
149//! ```
150//!
151//! This approach is useful when you need logging available before Tauri starts,
152//! or when you want full control over the subscriber configuration.
153//!
154//! ## Flamegraph Profiling
155//!
156//! The `flamegraph` feature enables performance profiling with flamegraph/flamechart
157//! visualizations.
158//!
159//! ### With Default Subscriber
160//!
161//! ```rust,no_run
162//! # use tauri_plugin_tracing::{Builder, LevelFilter};
163//! Builder::new()
164//! .with_max_level(LevelFilter::DEBUG)
165//! .with_flamegraph()
166//! .with_default_subscriber()
167//! .build::<tauri::Wry>();
168//! ```
169//!
170//! ### With Custom Subscriber
171//!
172//! Use [`create_flame_layer()`] to add flamegraph profiling to a custom subscriber:
173//!
174//! ```no_run
175//! use tauri_plugin_tracing::{Builder, WebviewLayer, LevelFilter, create_flame_layer};
176//! use tracing_subscriber::{Registry, layer::SubscriberExt, util::SubscriberInitExt, fmt};
177//!
178//! fn main() {
179//! let tracing_builder = Builder::new().with_max_level(LevelFilter::DEBUG);
180//! let filter = tracing_builder.build_filter();
181//!
182//! tauri::Builder::default()
183//! .plugin(tracing_builder.build())
184//! .setup(move |app| {
185//! let flame_layer = create_flame_layer(app.handle())?;
186//!
187//! Registry::default()
188//! .with(flame_layer) // Must be first - typed for Registry
189//! .with(fmt::layer())
190//! .with(WebviewLayer::new(app.handle().clone()))
191//! .with(filter)
192//! .init();
193//! Ok(())
194//! })
195//! .run(tauri::generate_context!("examples/default-subscriber/src-tauri/tauri.conf.json"))
196//! .expect("error while running tauri application");
197//! }
198//! ```
199//!
200//! ### Early Initialization with Flamegraph
201//!
202//! Use [`create_flame_layer_with_path()`] and [`FlameExt`] to initialize tracing
203//! before Tauri starts while still enabling frontend flamegraph generation:
204//!
205//! ```no_run
206//! use tauri_plugin_tracing::{Builder, create_flame_layer_with_path, FlameExt};
207//! use tracing_subscriber::{registry, layer::SubscriberExt, util::SubscriberInitExt, fmt};
208//!
209//! fn main() {
210//! let log_dir = std::env::temp_dir().join("my-app");
211//! std::fs::create_dir_all(&log_dir).unwrap();
212//!
213//! // Create flame layer before Tauri starts
214//! let (flame_layer, flame_guard) = create_flame_layer_with_path(
215//! &log_dir.join("profile.folded")
216//! ).unwrap();
217//!
218//! // Initialize tracing early
219//! registry()
220//! .with(flame_layer) // Must be first - typed for Registry
221//! .with(fmt::layer())
222//! .init();
223//!
224//! // Now start Tauri and register the guard
225//! tauri::Builder::default()
226//! .plugin(Builder::new().build())
227//! .setup(move |app| {
228//! // Register the guard so JS can generate flamegraphs
229//! app.handle().register_flamegraph(flame_guard)?;
230//! Ok(())
231//! })
232//! .run(tauri::generate_context!("examples/default-subscriber/src-tauri/tauri.conf.json"))
233//! .expect("error while running tauri application");
234//! }
235//! ```
236//!
237//! Then generate visualizations from JavaScript:
238//!
239//! ```javascript
240//! import { generateFlamegraph, generateFlamechart } from '@fltsci/tauri-plugin-tracing';
241//!
242//! // Generate a flamegraph (collapses identical stack frames)
243//! const flamegraphPath = await generateFlamegraph();
244//!
245//! // Generate a flamechart (preserves event ordering)
246//! const flamechartPath = await generateFlamechart();
247//! ```
248//!
249//! ## JavaScript API
250//!
251//! ```javascript
252//! import { trace, debug, info, warn, error } from '@fltsci/tauri-plugin-tracing';
253//!
254//! info('Application started');
255//! debug('Debug information', { key: 'value' });
256//! error('Something went wrong');
257//! ```
258
259mod callstack;
260mod commands;
261mod error;
262#[cfg(feature = "flamegraph")]
263mod flamegraph;
264mod layer;
265mod strip_ansi;
266mod types;
267
268use std::path::PathBuf;
269use tauri::plugin::{self, TauriPlugin};
270use tauri::{AppHandle, Manager, Runtime};
271use tracing_appender::non_blocking::WorkerGuard;
272use tracing_subscriber::{
273 Layer as _, Registry,
274 filter::{Targets, filter_fn},
275 fmt::{self, SubscriberBuilder},
276 layer::SubscriberExt,
277};
278
279// Re-export public types from modules
280pub use callstack::{CallStack, CallStackLine};
281pub use commands::log;
282pub use error::{Error, Result};
283pub use layer::{LogLevel, LogMessage, RecordPayload, WebviewLayer};
284pub use strip_ansi::{StripAnsiWriter, StripAnsiWriterGuard};
285pub use types::{
286 FormatOptions, LogFormat, MaxFileSize, Rotation, RotationStrategy, Target, TimezoneStrategy,
287};
288
289/// A boxed filter function for metadata-based log filtering.
290///
291/// This type alias represents a filter that examines event metadata to determine
292/// whether a log should be emitted. The function receives a reference to the
293/// metadata and returns `true` if the log should be included.
294pub type FilterFn = Box<dyn Fn(&tracing::Metadata<'_>) -> bool + Send + Sync>;
295
296/// A boxed tracing layer that can be added to the default subscriber.
297///
298/// Use this type with [`Builder::with_layer()`] to add custom tracing layers
299/// (e.g., for OpenTelemetry, Sentry, or custom logging integrations) to the
300/// plugin-managed subscriber.
301///
302/// # Example
303///
304/// ```rust,no_run
305/// use tauri_plugin_tracing::{Builder, BoxedLayer};
306/// use tracing_subscriber::Layer;
307///
308/// // Create a custom layer (e.g., from another crate) and box it
309/// let my_layer: BoxedLayer = tracing_subscriber::fmt::layer().boxed();
310///
311/// Builder::new()
312/// .with_layer(my_layer)
313/// .with_default_subscriber()
314/// .build::<tauri::Wry>();
315/// ```
316pub type BoxedLayer = Box<dyn tracing_subscriber::Layer<Registry> + Send + Sync + 'static>;
317
318#[cfg(feature = "flamegraph")]
319pub use flamegraph::*;
320
321/// Re-export of the [`tracing`] crate for convenience.
322pub use tracing;
323/// Re-export of the [`tracing_appender`] crate for file logging configuration.
324pub use tracing_appender;
325/// Re-export of the [`tracing_subscriber`] crate for subscriber configuration.
326pub use tracing_subscriber;
327
328/// Re-export of [`tracing_subscriber::filter::LevelFilter`] for configuring log levels.
329pub use tracing_subscriber::filter::LevelFilter;
330
331#[cfg(target_os = "ios")]
332mod ios {
333 swift_rs::swift!(pub fn tauri_log(
334 level: u8, message: *const std::ffi::c_void
335 ));
336}
337
338/// Stores the WorkerGuard to ensure logs are flushed on shutdown.
339/// This must be kept alive for the lifetime of the application.
340struct LogGuard(#[allow(dead_code)] Option<WorkerGuard>);
341
342/// Builder for configuring and creating the tracing plugin.
343///
344/// Use this builder to customize logging behavior before registering the plugin
345/// with your Tauri application.
346///
347/// # Example
348///
349/// ```rust,no_run
350/// use tauri_plugin_tracing::{Builder, LevelFilter};
351///
352/// let plugin = Builder::new()
353/// .with_max_level(LevelFilter::DEBUG)
354/// .with_target("hyper", LevelFilter::WARN) // Reduce noise from hyper
355/// .with_target("my_app", LevelFilter::TRACE) // Verbose logging for your app
356/// .build::<tauri::Wry>();
357/// ```
358pub struct Builder {
359 builder: SubscriberBuilder,
360 log_level: LevelFilter,
361 filter: Targets,
362 custom_filter: Option<FilterFn>,
363 custom_layer: Option<BoxedLayer>,
364 targets: Vec<Target>,
365 rotation: Rotation,
366 rotation_strategy: RotationStrategy,
367 max_file_size: Option<MaxFileSize>,
368 timezone_strategy: TimezoneStrategy,
369 log_format: LogFormat,
370 show_file: bool,
371 show_line_number: bool,
372 show_thread_ids: bool,
373 show_thread_names: bool,
374 show_target: bool,
375 show_level: bool,
376 set_default_subscriber: bool,
377 #[cfg(feature = "colored")]
378 use_colors: bool,
379 #[cfg(feature = "flamegraph")]
380 enable_flamegraph: bool,
381}
382
383impl Default for Builder {
384 fn default() -> Self {
385 Self {
386 builder: SubscriberBuilder::default(),
387 log_level: LevelFilter::WARN,
388 filter: Targets::default(),
389 custom_filter: None,
390 custom_layer: None,
391 targets: vec![Target::Stdout, Target::Webview],
392 rotation: Rotation::default(),
393 rotation_strategy: RotationStrategy::default(),
394 max_file_size: None,
395 timezone_strategy: TimezoneStrategy::default(),
396 log_format: LogFormat::default(),
397 show_file: false,
398 show_line_number: false,
399 show_thread_ids: false,
400 show_thread_names: false,
401 show_target: true,
402 show_level: true,
403 set_default_subscriber: false,
404 #[cfg(feature = "colored")]
405 use_colors: false,
406 #[cfg(feature = "flamegraph")]
407 enable_flamegraph: false,
408 }
409 }
410}
411
412impl Builder {
413 /// Creates a new builder with default settings.
414 ///
415 /// The default log level is [`LevelFilter::WARN`].
416 pub fn new() -> Self {
417 Default::default()
418 }
419
420 /// Sets the maximum log level.
421 ///
422 /// Events more verbose than this level will be filtered out.
423 ///
424 /// # Example
425 ///
426 /// ```rust,no_run
427 /// # use tauri_plugin_tracing::{Builder, LevelFilter};
428 /// Builder::new().with_max_level(LevelFilter::DEBUG);
429 /// ```
430 pub fn with_max_level(mut self, max_level: LevelFilter) -> Self {
431 self.log_level = max_level;
432 self.builder = self.builder.with_max_level(max_level);
433 self
434 }
435
436 /// Sets the log level for a specific target (module path).
437 ///
438 /// This allows fine-grained control over logging verbosity for different
439 /// parts of your application or dependencies.
440 ///
441 /// # Example
442 ///
443 /// ```rust,no_run
444 /// # use tauri_plugin_tracing::{Builder, LevelFilter};
445 /// Builder::new()
446 /// .with_max_level(LevelFilter::INFO)
447 /// .with_target("my_app::database", LevelFilter::DEBUG)
448 /// .with_target("hyper", LevelFilter::WARN);
449 /// ```
450 pub fn with_target(mut self, target: &str, level: LevelFilter) -> Self {
451 self.filter = self.filter.with_target(target, level);
452 self
453 }
454
455 /// Sets a custom filter function for metadata-based log filtering.
456 ///
457 /// The filter function receives the metadata for each log event and returns
458 /// `true` if the event should be logged. This filter is applied in addition
459 /// to the level and target filters configured via [`with_max_level()`](Self::with_max_level)
460 /// and [`with_target()`](Self::with_target).
461 ///
462 /// Only applies when using [`with_default_subscriber()`](Self::with_default_subscriber).
463 /// For custom subscribers, use [`tracing_subscriber::filter::filter_fn()`] directly.
464 ///
465 /// # Example
466 ///
467 /// ```rust,no_run
468 /// use tauri_plugin_tracing::Builder;
469 ///
470 /// // Filter out logs from a specific module
471 /// Builder::new()
472 /// .filter(|metadata| {
473 /// metadata.target() != "noisy_crate::spammy_module"
474 /// })
475 /// .with_default_subscriber()
476 /// .build::<tauri::Wry>();
477 ///
478 /// // Only log events (not spans)
479 /// Builder::new()
480 /// .filter(|metadata| metadata.is_event())
481 /// .with_default_subscriber()
482 /// .build::<tauri::Wry>();
483 /// ```
484 pub fn filter<F>(mut self, filter: F) -> Self
485 where
486 F: Fn(&tracing::Metadata<'_>) -> bool + Send + Sync + 'static,
487 {
488 self.custom_filter = Some(Box::new(filter));
489 self
490 }
491
492 /// Adds a custom tracing layer to the subscriber.
493 ///
494 /// Use this to integrate additional tracing functionality (e.g., OpenTelemetry,
495 /// Sentry, custom metrics) with the plugin-managed subscriber.
496 ///
497 /// Only applies when using [`with_default_subscriber()`](Self::with_default_subscriber).
498 ///
499 /// Note: Only one custom layer is supported. Calling this multiple times will
500 /// replace the previous layer. To use multiple custom layers, compose them
501 /// with [`tracing_subscriber::layer::Layered`] before passing to this method.
502 ///
503 /// # Example
504 ///
505 /// ```rust,no_run
506 /// use tauri_plugin_tracing::Builder;
507 /// use tracing_subscriber::Layer;
508 ///
509 /// // Add a custom layer (e.g., a secondary fmt layer or OpenTelemetry)
510 /// let custom_layer = tracing_subscriber::fmt::layer().boxed();
511 ///
512 /// Builder::new()
513 /// .with_layer(custom_layer)
514 /// .with_default_subscriber()
515 /// .build::<tauri::Wry>();
516 /// ```
517 pub fn with_layer(mut self, layer: BoxedLayer) -> Self {
518 self.custom_layer = Some(layer);
519 self
520 }
521
522 /// Enables flamegraph profiling.
523 ///
524 /// When enabled, tracing spans are recorded to a folded stack format file
525 /// that can be converted to a flamegraph or flamechart visualization.
526 ///
527 /// The folded stack data is written to `{app_log_dir}/profile.folded`.
528 /// Use the `generate_flamegraph` or `generate_flamechart` commands to
529 /// convert this data to an SVG visualization.
530 ///
531 /// Only applies when using [`with_default_subscriber()`](Self::with_default_subscriber).
532 ///
533 /// # Example
534 ///
535 /// ```no_run
536 /// use tauri_plugin_tracing::Builder;
537 ///
538 /// let _plugin = Builder::new()
539 /// .with_flamegraph()
540 /// .with_default_subscriber()
541 /// .build::<tauri::Wry>();
542 /// ```
543 #[cfg(feature = "flamegraph")]
544 pub fn with_flamegraph(mut self) -> Self {
545 self.enable_flamegraph = true;
546 self
547 }
548
549 /// Enables colored output in the terminal.
550 ///
551 /// This adds ANSI color codes to log level indicators.
552 /// Only available when the `colored` feature is enabled.
553 /// Only applies when using [`with_default_subscriber()`](Self::with_default_subscriber).
554 #[cfg(feature = "colored")]
555 pub fn with_colors(mut self) -> Self {
556 self.builder = self.builder.with_ansi(true);
557 self.use_colors = true;
558 self
559 }
560
561 /// Enables file logging to the platform-standard log directory.
562 ///
563 /// Log files rotate daily with the naming pattern `app.YYYY-MM-DD.log`.
564 ///
565 /// Platform log directories:
566 /// - **macOS**: `~/Library/Logs/{bundle_identifier}`
567 /// - **Linux**: `~/.local/share/{bundle_identifier}/logs`
568 /// - **Windows**: `%LOCALAPPDATA%/{bundle_identifier}/logs`
569 ///
570 /// This is a convenience method equivalent to calling
571 /// `.target(Target::LogDir { file_name: None })`.
572 ///
573 /// # Example
574 ///
575 /// ```rust,no_run
576 /// # use tauri_plugin_tracing::{Builder, LevelFilter};
577 /// Builder::new()
578 /// .with_max_level(LevelFilter::DEBUG)
579 /// .with_file_logging()
580 /// .build::<tauri::Wry>();
581 /// ```
582 pub fn with_file_logging(self) -> Self {
583 self.target(Target::LogDir { file_name: None })
584 }
585
586 /// Sets the rotation period for log files.
587 ///
588 /// This controls how often new log files are created. Only applies when
589 /// file logging is enabled.
590 ///
591 /// # Example
592 ///
593 /// ```rust,no_run
594 /// use tauri_plugin_tracing::{Builder, Rotation};
595 ///
596 /// Builder::new()
597 /// .with_file_logging()
598 /// .with_rotation(Rotation::Hourly) // Rotate every hour
599 /// .build::<tauri::Wry>();
600 /// ```
601 pub fn with_rotation(mut self, rotation: Rotation) -> Self {
602 self.rotation = rotation;
603 self
604 }
605
606 /// Sets the retention strategy for rotated log files.
607 ///
608 /// This controls how many old log files are kept. Cleanup happens when
609 /// the application starts.
610 ///
611 /// # Example
612 ///
613 /// ```rust,no_run
614 /// use tauri_plugin_tracing::{Builder, RotationStrategy};
615 ///
616 /// Builder::new()
617 /// .with_file_logging()
618 /// .with_rotation_strategy(RotationStrategy::KeepSome(7)) // Keep 7 files
619 /// .build::<tauri::Wry>();
620 /// ```
621 pub fn with_rotation_strategy(mut self, strategy: RotationStrategy) -> Self {
622 self.rotation_strategy = strategy;
623 self
624 }
625
626 /// Sets the maximum file size before rotating.
627 ///
628 /// When set, log files will rotate when they reach this size, in addition
629 /// to any time-based rotation configured via [`with_rotation()`](Self::with_rotation).
630 ///
631 /// Use [`MaxFileSize`] for convenient size specification:
632 /// - `MaxFileSize::kb(100)` - 100 kilobytes
633 /// - `MaxFileSize::mb(10)` - 10 megabytes
634 /// - `MaxFileSize::gb(1)` - 1 gigabyte
635 ///
636 /// # Example
637 ///
638 /// ```rust,no_run
639 /// use tauri_plugin_tracing::{Builder, MaxFileSize};
640 ///
641 /// // Rotate when file reaches 10 MB
642 /// Builder::new()
643 /// .with_file_logging()
644 /// .with_max_file_size(MaxFileSize::mb(10))
645 /// .build::<tauri::Wry>();
646 /// ```
647 pub fn with_max_file_size(mut self, size: MaxFileSize) -> Self {
648 self.max_file_size = Some(size);
649 self
650 }
651
652 /// Sets the timezone strategy for log timestamps.
653 ///
654 /// Controls whether timestamps are displayed in UTC or local time.
655 /// The default is [`TimezoneStrategy::Utc`].
656 ///
657 /// # Example
658 ///
659 /// ```rust,no_run
660 /// use tauri_plugin_tracing::{Builder, TimezoneStrategy};
661 ///
662 /// // Use local time for timestamps
663 /// Builder::new()
664 /// .with_timezone_strategy(TimezoneStrategy::Local)
665 /// .build::<tauri::Wry>();
666 /// ```
667 pub fn with_timezone_strategy(mut self, strategy: TimezoneStrategy) -> Self {
668 self.timezone_strategy = strategy;
669 self
670 }
671
672 /// Sets the log output format style.
673 ///
674 /// Controls the overall structure of log output. The default is [`LogFormat::Full`].
675 /// Only applies when using [`with_default_subscriber()`](Self::with_default_subscriber).
676 ///
677 /// # Example
678 ///
679 /// ```rust,no_run
680 /// use tauri_plugin_tracing::{Builder, LogFormat};
681 ///
682 /// // Use compact format for shorter lines
683 /// Builder::new()
684 /// .with_format(LogFormat::Compact)
685 /// .with_default_subscriber()
686 /// .build::<tauri::Wry>();
687 /// ```
688 pub fn with_format(mut self, format: LogFormat) -> Self {
689 self.log_format = format;
690 self
691 }
692
693 /// Sets whether to include the source file path in log output.
694 ///
695 /// When enabled, logs will show which file the log event originated from.
696 /// Default is `false`.
697 ///
698 /// Only applies when using [`with_default_subscriber()`](Self::with_default_subscriber).
699 ///
700 /// # Example
701 ///
702 /// ```rust,no_run
703 /// # use tauri_plugin_tracing::Builder;
704 /// Builder::new()
705 /// .with_file(true)
706 /// .with_default_subscriber()
707 /// .build::<tauri::Wry>();
708 /// ```
709 pub fn with_file(mut self, show: bool) -> Self {
710 self.show_file = show;
711 self
712 }
713
714 /// Sets whether to include the source line number in log output.
715 ///
716 /// When enabled, logs will show which line number the log event originated from.
717 /// Default is `false`.
718 ///
719 /// Only applies when using [`with_default_subscriber()`](Self::with_default_subscriber).
720 ///
721 /// # Example
722 ///
723 /// ```rust,no_run
724 /// # use tauri_plugin_tracing::Builder;
725 /// Builder::new()
726 /// .with_line_number(true)
727 /// .with_default_subscriber()
728 /// .build::<tauri::Wry>();
729 /// ```
730 pub fn with_line_number(mut self, show: bool) -> Self {
731 self.show_line_number = show;
732 self
733 }
734
735 /// Sets whether to include the current thread ID in log output.
736 ///
737 /// When enabled, logs will show the ID of the thread that emitted the event.
738 /// Default is `false`.
739 ///
740 /// Only applies when using [`with_default_subscriber()`](Self::with_default_subscriber).
741 ///
742 /// # Example
743 ///
744 /// ```rust,no_run
745 /// # use tauri_plugin_tracing::Builder;
746 /// Builder::new()
747 /// .with_thread_ids(true)
748 /// .with_default_subscriber()
749 /// .build::<tauri::Wry>();
750 /// ```
751 pub fn with_thread_ids(mut self, show: bool) -> Self {
752 self.show_thread_ids = show;
753 self
754 }
755
756 /// Sets whether to include the current thread name in log output.
757 ///
758 /// When enabled, logs will show the name of the thread that emitted the event.
759 /// Default is `false`.
760 ///
761 /// Only applies when using [`with_default_subscriber()`](Self::with_default_subscriber).
762 ///
763 /// # Example
764 ///
765 /// ```rust,no_run
766 /// # use tauri_plugin_tracing::Builder;
767 /// Builder::new()
768 /// .with_thread_names(true)
769 /// .with_default_subscriber()
770 /// .build::<tauri::Wry>();
771 /// ```
772 pub fn with_thread_names(mut self, show: bool) -> Self {
773 self.show_thread_names = show;
774 self
775 }
776
777 /// Sets whether to include the log target (module path) in log output.
778 ///
779 /// When enabled, logs will show which module/target emitted the event.
780 /// Default is `true`.
781 ///
782 /// Only applies when using [`with_default_subscriber()`](Self::with_default_subscriber).
783 ///
784 /// # Example
785 ///
786 /// ```rust,no_run
787 /// # use tauri_plugin_tracing::Builder;
788 /// // Disable target display for cleaner output
789 /// Builder::new()
790 /// .with_target_display(false)
791 /// .with_default_subscriber()
792 /// .build::<tauri::Wry>();
793 /// ```
794 pub fn with_target_display(mut self, show: bool) -> Self {
795 self.show_target = show;
796 self
797 }
798
799 /// Sets whether to include the log level in log output.
800 ///
801 /// When enabled, logs will show the severity level (TRACE, DEBUG, INFO, etc.).
802 /// Default is `true`.
803 ///
804 /// Only applies when using [`with_default_subscriber()`](Self::with_default_subscriber).
805 ///
806 /// # Example
807 ///
808 /// ```rust,no_run
809 /// # use tauri_plugin_tracing::Builder;
810 /// // Disable level display
811 /// Builder::new()
812 /// .with_level(false)
813 /// .with_default_subscriber()
814 /// .build::<tauri::Wry>();
815 /// ```
816 pub fn with_level(mut self, show: bool) -> Self {
817 self.show_level = show;
818 self
819 }
820
821 /// Adds a log output target.
822 ///
823 /// By default, logs are sent to [`Target::Stdout`] and [`Target::Webview`].
824 /// Use this method to add additional targets.
825 ///
826 /// # Example
827 ///
828 /// ```rust,no_run
829 /// use tauri_plugin_tracing::{Builder, Target};
830 ///
831 /// // Add file logging to the default targets
832 /// Builder::new()
833 /// .target(Target::LogDir { file_name: None })
834 /// .build::<tauri::Wry>();
835 /// ```
836 pub fn target(mut self, target: Target) -> Self {
837 self.targets.push(target);
838 self
839 }
840
841 /// Sets the log output targets, replacing any previously configured targets.
842 ///
843 /// By default, logs are sent to [`Target::Stdout`] and [`Target::Webview`].
844 /// Use this method to completely replace the default targets.
845 ///
846 /// # Example
847 ///
848 /// ```rust,no_run
849 /// use tauri_plugin_tracing::{Builder, Target};
850 ///
851 /// // Log only to file and webview (no stdout)
852 /// Builder::new()
853 /// .targets([
854 /// Target::LogDir { file_name: None },
855 /// Target::Webview,
856 /// ])
857 /// .build::<tauri::Wry>();
858 ///
859 /// // Log only to stderr
860 /// Builder::new()
861 /// .targets([Target::Stderr])
862 /// .build::<tauri::Wry>();
863 /// ```
864 pub fn targets(mut self, targets: impl IntoIterator<Item = Target>) -> Self {
865 self.targets = targets.into_iter().collect();
866 self
867 }
868
869 /// Removes all configured log targets.
870 ///
871 /// Use this followed by [`target()`](Self::target) to build a custom set
872 /// of targets from scratch.
873 ///
874 /// # Example
875 ///
876 /// ```rust,no_run
877 /// use tauri_plugin_tracing::{Builder, Target};
878 ///
879 /// // Start fresh and only log to webview
880 /// Builder::new()
881 /// .clear_targets()
882 /// .target(Target::Webview)
883 /// .build::<tauri::Wry>();
884 /// ```
885 pub fn clear_targets(mut self) -> Self {
886 self.targets.clear();
887 self
888 }
889
890 /// Enables the plugin to set up and register the global tracing subscriber.
891 ///
892 /// By default, this plugin does **not** call [`tracing::subscriber::set_global_default()`],
893 /// following the convention that libraries should not set globals. This allows your
894 /// application to compose its own subscriber with layers from multiple crates.
895 ///
896 /// Call this method if you want the plugin to handle all tracing setup for you,
897 /// using the configuration from this builder (log levels, targets, file logging, etc.).
898 ///
899 /// # Example
900 ///
901 /// ```rust,no_run
902 /// # use tauri_plugin_tracing::{Builder, LevelFilter};
903 /// // Let the plugin set up everything
904 /// tauri::Builder::default()
905 /// .plugin(
906 /// Builder::new()
907 /// .with_max_level(LevelFilter::DEBUG)
908 /// .with_file_logging()
909 /// .with_default_subscriber() // Opt-in to global subscriber
910 /// .build()
911 /// );
912 /// // .run(tauri::generate_context!("examples/default-subscriber/src-tauri/tauri.conf.json"))
913 /// ```
914 pub fn with_default_subscriber(mut self) -> Self {
915 self.set_default_subscriber = true;
916 self
917 }
918
919 /// Returns the configured log output targets.
920 ///
921 /// Use this when setting up your own subscriber to determine which
922 /// layers to include based on the configured targets.
923 ///
924 /// # Example
925 ///
926 /// ```rust,no_run
927 /// use tauri_plugin_tracing::{Builder, Target};
928 ///
929 /// let builder = Builder::new()
930 /// .target(Target::LogDir { file_name: None });
931 ///
932 /// for target in builder.configured_targets() {
933 /// match target {
934 /// Target::Stdout => { /* add stdout layer */ }
935 /// Target::Stderr => { /* add stderr layer */ }
936 /// Target::Webview => { /* add WebviewLayer */ }
937 /// Target::LogDir { .. } | Target::Folder { .. } => { /* add file layer */ }
938 /// }
939 /// }
940 /// ```
941 pub fn configured_targets(&self) -> &[Target] {
942 &self.targets
943 }
944
945 /// Returns the configured rotation period for file logging.
946 pub fn configured_rotation(&self) -> Rotation {
947 self.rotation
948 }
949
950 /// Returns the configured rotation strategy for file logging.
951 pub fn configured_rotation_strategy(&self) -> RotationStrategy {
952 self.rotation_strategy
953 }
954
955 /// Returns the configured maximum file size for rotation, if set.
956 pub fn configured_max_file_size(&self) -> Option<MaxFileSize> {
957 self.max_file_size
958 }
959
960 /// Returns the configured timezone strategy for timestamps.
961 pub fn configured_timezone_strategy(&self) -> TimezoneStrategy {
962 self.timezone_strategy
963 }
964
965 /// Returns the configured log format style.
966 pub fn configured_format(&self) -> LogFormat {
967 self.log_format
968 }
969
970 /// Returns the configured format options.
971 pub fn configured_format_options(&self) -> FormatOptions {
972 FormatOptions {
973 format: self.log_format,
974 file: self.show_file,
975 line_number: self.show_line_number,
976 thread_ids: self.show_thread_ids,
977 thread_names: self.show_thread_names,
978 target: self.show_target,
979 level: self.show_level,
980 }
981 }
982
983 /// Returns the configured filter based on log level and per-target settings.
984 ///
985 /// Use this when setting up your own subscriber to apply the same filtering
986 /// configured via [`with_max_level()`](Self::with_max_level) and
987 /// [`with_target()`](Self::with_target).
988 ///
989 /// # Example
990 ///
991 /// ```rust,no_run
992 /// # use tauri_plugin_tracing::{Builder, WebviewLayer, LevelFilter};
993 /// # use tracing_subscriber::{Registry, layer::SubscriberExt, util::SubscriberInitExt, fmt};
994 /// let builder = Builder::new()
995 /// .with_max_level(LevelFilter::DEBUG)
996 /// .with_target("hyper", LevelFilter::WARN);
997 ///
998 /// let filter = builder.build_filter();
999 ///
1000 /// tauri::Builder::default()
1001 /// .plugin(builder.build())
1002 /// .setup(move |app| {
1003 /// Registry::default()
1004 /// .with(fmt::layer())
1005 /// .with(WebviewLayer::new(app.handle().clone()))
1006 /// .with(filter)
1007 /// .init();
1008 /// Ok(())
1009 /// });
1010 /// // .run(tauri::generate_context!("examples/default-subscriber/src-tauri/tauri.conf.json"))
1011 /// ```
1012 pub fn build_filter(&self) -> Targets {
1013 self.filter.clone().with_default(self.log_level)
1014 }
1015
1016 #[cfg(feature = "flamegraph")]
1017 fn plugin_builder<R: Runtime>() -> plugin::Builder<R> {
1018 plugin::Builder::new("tracing").invoke_handler(tauri::generate_handler![
1019 commands::log,
1020 commands::generate_flamegraph,
1021 commands::generate_flamechart
1022 ])
1023 }
1024
1025 #[cfg(not(feature = "flamegraph"))]
1026 fn plugin_builder<R: Runtime>() -> plugin::Builder<R> {
1027 plugin::Builder::new("tracing").invoke_handler(tauri::generate_handler![commands::log,])
1028 }
1029
1030 /// Builds and returns the configured Tauri plugin.
1031 ///
1032 /// This consumes the builder and returns a [`TauriPlugin`] that can be
1033 /// registered with your Tauri application.
1034 ///
1035 /// # Example
1036 ///
1037 /// ```rust,no_run
1038 /// # use tauri_plugin_tracing::Builder;
1039 /// tauri::Builder::default()
1040 /// .plugin(Builder::new().build());
1041 /// // .run(tauri::generate_context!("examples/default-subscriber/src-tauri/tauri.conf.json"))
1042 /// ```
1043 pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
1044 let log_level = self.log_level;
1045 let filter = self.filter;
1046 let custom_filter = self.custom_filter;
1047 let custom_layer = self.custom_layer;
1048 let targets = self.targets;
1049 let rotation = self.rotation;
1050 let rotation_strategy = self.rotation_strategy;
1051 let max_file_size = self.max_file_size;
1052 let timezone_strategy = self.timezone_strategy;
1053 let format_options = FormatOptions {
1054 format: self.log_format,
1055 file: self.show_file,
1056 line_number: self.show_line_number,
1057 thread_ids: self.show_thread_ids,
1058 thread_names: self.show_thread_names,
1059 target: self.show_target,
1060 level: self.show_level,
1061 };
1062 let set_default_subscriber = self.set_default_subscriber;
1063
1064 #[cfg(feature = "colored")]
1065 let use_colors = self.use_colors;
1066
1067 #[cfg(feature = "flamegraph")]
1068 let enable_flamegraph = self.enable_flamegraph;
1069
1070 Self::plugin_builder()
1071 .setup(move |app, _api| {
1072 #[cfg(feature = "flamegraph")]
1073 setup_flamegraph(app);
1074
1075 #[cfg(desktop)]
1076 if set_default_subscriber {
1077 let guard = acquire_logger(
1078 app,
1079 log_level,
1080 filter,
1081 custom_filter,
1082 custom_layer,
1083 &targets,
1084 rotation,
1085 rotation_strategy,
1086 max_file_size,
1087 timezone_strategy,
1088 format_options,
1089 #[cfg(feature = "colored")]
1090 use_colors,
1091 #[cfg(feature = "flamegraph")]
1092 enable_flamegraph,
1093 )?;
1094
1095 // Store the guard in Tauri's state management to ensure logs flush on shutdown
1096 if guard.is_some() {
1097 app.manage(LogGuard(guard));
1098 }
1099 }
1100
1101 Ok(())
1102 })
1103 .build()
1104 }
1105}
1106
1107/// Configuration for a file logging target.
1108struct FileTargetConfig {
1109 log_dir: PathBuf,
1110 file_name: String,
1111}
1112
1113/// Resolves file target configuration from a Target.
1114fn resolve_file_target<R: Runtime>(
1115 app_handle: &AppHandle<R>,
1116 target: &Target,
1117) -> Result<Option<FileTargetConfig>> {
1118 match target {
1119 Target::LogDir { file_name } => {
1120 let log_dir = app_handle.path().app_log_dir()?;
1121 std::fs::create_dir_all(&log_dir)?;
1122 Ok(Some(FileTargetConfig {
1123 log_dir,
1124 file_name: file_name.clone().unwrap_or_else(|| "app".to_string()),
1125 }))
1126 }
1127 Target::Folder { path, file_name } => {
1128 std::fs::create_dir_all(path)?;
1129 Ok(Some(FileTargetConfig {
1130 log_dir: path.clone(),
1131 file_name: file_name.clone().unwrap_or_else(|| "app".to_string()),
1132 }))
1133 }
1134 _ => Ok(None),
1135 }
1136}
1137
1138/// Cleans up old log files based on the retention strategy.
1139fn cleanup_old_logs(
1140 log_dir: &std::path::Path,
1141 file_prefix: &str,
1142 strategy: RotationStrategy,
1143) -> Result<()> {
1144 match strategy {
1145 RotationStrategy::KeepAll => Ok(()),
1146 RotationStrategy::KeepOne => cleanup_logs_keeping(log_dir, file_prefix, 1),
1147 RotationStrategy::KeepSome(n) => cleanup_logs_keeping(log_dir, file_prefix, n as usize),
1148 }
1149}
1150
1151/// Helper to delete old log files, keeping only the most recent `keep` files.
1152fn cleanup_logs_keeping(log_dir: &std::path::Path, file_prefix: &str, keep: usize) -> Result<()> {
1153 let prefix_with_dot = format!("{}.", file_prefix);
1154 let mut log_files: Vec<_> = std::fs::read_dir(log_dir)?
1155 .filter_map(|entry| entry.ok())
1156 .filter(|entry| {
1157 entry
1158 .file_name()
1159 .to_str()
1160 .is_some_and(|name| name.starts_with(&prefix_with_dot) && name.ends_with(".log"))
1161 })
1162 .collect();
1163
1164 // Sort by filename (which includes date) in descending order (newest first)
1165 log_files.sort_by_key(|entry| std::cmp::Reverse(entry.file_name()));
1166
1167 // Delete all but the most recent `keep` files
1168 for entry in log_files.into_iter().skip(keep) {
1169 if let Err(e) = std::fs::remove_file(entry.path()) {
1170 tracing::warn!("Failed to remove old log file {:?}: {}", entry.path(), e);
1171 }
1172 }
1173
1174 Ok(())
1175}
1176
1177/// Sets up the tracing subscriber based on configured targets.
1178#[cfg(desktop)]
1179#[allow(clippy::too_many_arguments)]
1180fn acquire_logger<R: Runtime>(
1181 app_handle: &AppHandle<R>,
1182 log_level: LevelFilter,
1183 filter: Targets,
1184 custom_filter: Option<FilterFn>,
1185 custom_layer: Option<BoxedLayer>,
1186 targets: &[Target],
1187 rotation: Rotation,
1188 rotation_strategy: RotationStrategy,
1189 max_file_size: Option<MaxFileSize>,
1190 timezone_strategy: TimezoneStrategy,
1191 format_options: FormatOptions,
1192 #[cfg(feature = "colored")] use_colors: bool,
1193 #[cfg(feature = "flamegraph")] enable_flamegraph: bool,
1194) -> Result<Option<WorkerGuard>> {
1195 use std::io;
1196 use tracing_subscriber::fmt::time::OffsetTime;
1197
1198 let filter_with_default = filter.with_default(log_level);
1199
1200 // Determine which targets are enabled
1201 let has_stdout = targets.iter().any(|t| matches!(t, Target::Stdout));
1202 let has_stderr = targets.iter().any(|t| matches!(t, Target::Stderr));
1203 let has_webview = targets.iter().any(|t| matches!(t, Target::Webview));
1204
1205 // Find file target (only first one is used)
1206 let file_config = targets
1207 .iter()
1208 .find_map(|t| resolve_file_target(app_handle, t).transpose())
1209 .transpose()?;
1210
1211 // Determine if ANSI should be enabled for stdout/stderr.
1212 // File output uses StripAnsiWriter to strip ANSI codes, so stdout can use colors.
1213 #[cfg(feature = "colored")]
1214 let use_ansi = use_colors;
1215 #[cfg(not(feature = "colored"))]
1216 let use_ansi = false;
1217
1218 // Helper to create timer based on timezone strategy
1219 let make_timer = || match timezone_strategy {
1220 TimezoneStrategy::Utc => OffsetTime::new(
1221 time::UtcOffset::UTC,
1222 time::format_description::well_known::Rfc3339,
1223 ),
1224 TimezoneStrategy::Local => time::UtcOffset::current_local_offset()
1225 .map(|offset| OffsetTime::new(offset, time::format_description::well_known::Rfc3339))
1226 .unwrap_or_else(|_| {
1227 OffsetTime::new(
1228 time::UtcOffset::UTC,
1229 time::format_description::well_known::Rfc3339,
1230 )
1231 }),
1232 };
1233
1234 // Macro to create a formatted layer with the appropriate format style.
1235 // This is needed because .compact() and .pretty() return different types.
1236 macro_rules! make_layer {
1237 ($layer:expr, $format:expr) => {
1238 match $format {
1239 LogFormat::Full => $layer.boxed(),
1240 LogFormat::Compact => $layer.compact().boxed(),
1241 LogFormat::Pretty => $layer.pretty().boxed(),
1242 }
1243 };
1244 }
1245
1246 // Create optional layers based on targets
1247 let stdout_layer = if has_stdout {
1248 let layer = fmt::layer()
1249 .with_timer(make_timer())
1250 .with_ansi(use_ansi)
1251 .with_file(format_options.file)
1252 .with_line_number(format_options.line_number)
1253 .with_thread_ids(format_options.thread_ids)
1254 .with_thread_names(format_options.thread_names)
1255 .with_target(format_options.target)
1256 .with_level(format_options.level);
1257 Some(make_layer!(layer, format_options.format))
1258 } else {
1259 None
1260 };
1261
1262 let stderr_layer = if has_stderr {
1263 let layer = fmt::layer()
1264 .with_timer(make_timer())
1265 .with_ansi(use_ansi)
1266 .with_file(format_options.file)
1267 .with_line_number(format_options.line_number)
1268 .with_thread_ids(format_options.thread_ids)
1269 .with_thread_names(format_options.thread_names)
1270 .with_target(format_options.target)
1271 .with_level(format_options.level)
1272 .with_writer(io::stderr);
1273 Some(make_layer!(layer, format_options.format))
1274 } else {
1275 None
1276 };
1277
1278 let webview_layer = if has_webview {
1279 Some(WebviewLayer::new(app_handle.clone()))
1280 } else {
1281 None
1282 };
1283
1284 // Set up file logging if configured
1285 let (file_layer, guard) = if let Some(config) = file_config {
1286 // Note: cleanup_old_logs only works reliably with time-based rotation
1287 // When using size-based rotation, files have numeric suffixes that may not sort correctly
1288 if max_file_size.is_none() {
1289 cleanup_old_logs(&config.log_dir, &config.file_name, rotation_strategy)?;
1290 }
1291
1292 // Use rolling-file crate when max_file_size is set (supports both size and time-based rotation)
1293 // Otherwise use tracing-appender (time-based only)
1294 if let Some(max_size) = max_file_size {
1295 use rolling_file::{BasicRollingFileAppender, RollingConditionBasic};
1296
1297 // Build rolling condition with both time and size triggers
1298 let mut condition = RollingConditionBasic::new();
1299 condition = match rotation {
1300 Rotation::Daily => condition.daily(),
1301 Rotation::Hourly => condition.hourly(),
1302 Rotation::Minutely => condition, // rolling-file doesn't have minutely, use size only
1303 Rotation::Never => condition, // size-only rotation
1304 };
1305 condition = condition.max_size(max_size.0);
1306
1307 // Determine max file count from rotation strategy
1308 let max_files = match rotation_strategy {
1309 RotationStrategy::KeepAll => u32::MAX as usize,
1310 RotationStrategy::KeepOne => 1,
1311 RotationStrategy::KeepSome(n) => n as usize,
1312 };
1313
1314 let log_path = config.log_dir.join(format!("{}.log", config.file_name));
1315 let file_appender = BasicRollingFileAppender::new(log_path, condition, max_files)
1316 .map_err(std::io::Error::other)?;
1317
1318 let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
1319 // Wrap with StripAnsiWriter to remove ANSI codes that leak from shared span formatting
1320 let strip_ansi_writer = StripAnsiWriter::new(non_blocking);
1321
1322 let layer = fmt::layer()
1323 .with_timer(make_timer())
1324 .with_ansi(false)
1325 .with_file(format_options.file)
1326 .with_line_number(format_options.line_number)
1327 .with_thread_ids(format_options.thread_ids)
1328 .with_thread_names(format_options.thread_names)
1329 .with_target(format_options.target)
1330 .with_level(format_options.level)
1331 .with_writer(strip_ansi_writer);
1332
1333 (Some(make_layer!(layer, format_options.format)), Some(guard))
1334 } else {
1335 // Time-based rotation only using tracing-appender with proper .log extension
1336 use tracing_appender::rolling::RollingFileAppender;
1337
1338 let appender_rotation = match rotation {
1339 Rotation::Daily => tracing_appender::rolling::Rotation::DAILY,
1340 Rotation::Hourly => tracing_appender::rolling::Rotation::HOURLY,
1341 Rotation::Minutely => tracing_appender::rolling::Rotation::MINUTELY,
1342 Rotation::Never => tracing_appender::rolling::Rotation::NEVER,
1343 };
1344
1345 let file_appender = RollingFileAppender::builder()
1346 .rotation(appender_rotation)
1347 .filename_prefix(&config.file_name)
1348 .filename_suffix("log")
1349 .build(&config.log_dir)
1350 .map_err(std::io::Error::other)?;
1351
1352 let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
1353 // Wrap with StripAnsiWriter to remove ANSI codes that leak from shared span formatting
1354 let strip_ansi_writer = StripAnsiWriter::new(non_blocking);
1355
1356 let layer = fmt::layer()
1357 .with_timer(make_timer())
1358 .with_ansi(false)
1359 .with_file(format_options.file)
1360 .with_line_number(format_options.line_number)
1361 .with_thread_ids(format_options.thread_ids)
1362 .with_thread_names(format_options.thread_names)
1363 .with_target(format_options.target)
1364 .with_level(format_options.level)
1365 .with_writer(strip_ansi_writer);
1366
1367 (Some(make_layer!(layer, format_options.format)), Some(guard))
1368 }
1369 } else {
1370 (None, None)
1371 };
1372
1373 // Create flame layer if flamegraph feature is enabled
1374 #[cfg(feature = "flamegraph")]
1375 let flame_layer = if enable_flamegraph {
1376 Some(create_flame_layer(app_handle)?)
1377 } else {
1378 None
1379 };
1380
1381 // Create custom filter layer if configured
1382 let custom_filter_layer = custom_filter.map(|f| filter_fn(move |metadata| f(metadata)));
1383
1384 // Compose the subscriber with all optional layers
1385 // Note: Boxed layers (custom_layer, flame_layer) must be combined and added first
1386 // because they're typed as Layer<Registry> and the subscriber type changes after each .with()
1387 #[cfg(feature = "flamegraph")]
1388 let combined_boxed_layer: Option<BoxedLayer> = match (custom_layer, flame_layer) {
1389 (Some(c), Some(f)) => {
1390 use tracing_subscriber::Layer;
1391 Some(c.and_then(f).boxed())
1392 }
1393 (Some(c), None) => Some(c),
1394 (None, Some(f)) => Some(f),
1395 (None, None) => None,
1396 };
1397
1398 #[cfg(not(feature = "flamegraph"))]
1399 let combined_boxed_layer = custom_layer;
1400
1401 let subscriber = Registry::default()
1402 .with(combined_boxed_layer)
1403 .with(stdout_layer)
1404 .with(stderr_layer)
1405 .with(file_layer)
1406 .with(webview_layer)
1407 .with(custom_filter_layer)
1408 .with(filter_with_default);
1409
1410 tracing::subscriber::set_global_default(subscriber)?;
1411 tracing::info!("tracing initialized");
1412 Ok(guard)
1413}