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}