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}