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