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}