tauri_plugin_tracing/
lib.rs

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