#![allow(clippy::needless_doctest_main)]
mod callstack;
mod commands;
mod error;
#[cfg(feature = "flamegraph")]
mod flamegraph;
mod layer;
#[cfg(feature = "profiling")]
mod profiling;
mod strip_ansi;
mod types;
use std::path::PathBuf;
use tauri::plugin::{self, TauriPlugin};
use tauri::{AppHandle, Manager, Runtime};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{
Layer as _, Registry,
filter::{Targets, filter_fn},
fmt::{self, SubscriberBuilder},
layer::SubscriberExt,
};
pub use callstack::{CallStack, CallStackLine};
pub use commands::log;
pub use error::{Error, Result};
pub use layer::{LogLevel, LogMessage, RecordPayload, WebviewLayer};
pub use strip_ansi::{StripAnsiWriter, StripAnsiWriterGuard};
pub use types::{
FormatOptions, LogFormat, MaxFileSize, Rotation, RotationStrategy, Target, TimezoneStrategy,
};
pub type FilterFn = Box<dyn Fn(&tracing::Metadata<'_>) -> bool + Send + Sync>;
pub type BoxedLayer = Box<dyn tracing_subscriber::Layer<Registry> + Send + Sync + 'static>;
#[cfg(feature = "flamegraph")]
pub use flamegraph::*;
#[cfg(feature = "profiling")]
pub use profiling::*;
pub use tracing;
pub use tracing_appender;
pub use tracing_subscriber;
pub use tracing_subscriber::filter::LevelFilter;
#[cfg(target_os = "ios")]
mod ios {
swift_rs::swift!(pub fn tauri_log(
level: u8, message: *const std::ffi::c_void
));
}
struct LogGuard(#[allow(dead_code)] Option<WorkerGuard>);
pub struct Builder {
builder: SubscriberBuilder,
log_level: LevelFilter,
filter: Targets,
custom_filter: Option<FilterFn>,
custom_layer: Option<BoxedLayer>,
targets: Vec<Target>,
rotation: Rotation,
rotation_strategy: RotationStrategy,
max_file_size: Option<MaxFileSize>,
timezone_strategy: TimezoneStrategy,
log_format: LogFormat,
show_file: bool,
show_line_number: bool,
show_thread_ids: bool,
show_thread_names: bool,
show_target: bool,
show_level: bool,
set_default_subscriber: bool,
#[cfg(feature = "colored")]
use_colors: bool,
#[cfg(feature = "flamegraph")]
enable_flamegraph: bool,
}
impl Default for Builder {
fn default() -> Self {
Self {
builder: SubscriberBuilder::default(),
log_level: LevelFilter::WARN,
filter: Targets::default(),
custom_filter: None,
custom_layer: None,
targets: vec![Target::Stdout, Target::Webview],
rotation: Rotation::default(),
rotation_strategy: RotationStrategy::default(),
max_file_size: None,
timezone_strategy: TimezoneStrategy::default(),
log_format: LogFormat::default(),
show_file: false,
show_line_number: false,
show_thread_ids: false,
show_thread_names: false,
show_target: true,
show_level: true,
set_default_subscriber: false,
#[cfg(feature = "colored")]
use_colors: false,
#[cfg(feature = "flamegraph")]
enable_flamegraph: false,
}
}
}
impl Builder {
pub fn new() -> Self {
Default::default()
}
pub fn with_max_level(mut self, max_level: LevelFilter) -> Self {
self.log_level = max_level;
self.builder = self.builder.with_max_level(max_level);
self
}
pub fn with_target(mut self, target: &str, level: LevelFilter) -> Self {
self.filter = self.filter.with_target(target, level);
self
}
pub fn filter<F>(mut self, filter: F) -> Self
where
F: Fn(&tracing::Metadata<'_>) -> bool + Send + Sync + 'static,
{
self.custom_filter = Some(Box::new(filter));
self
}
pub fn with_layer(mut self, layer: BoxedLayer) -> Self {
self.custom_layer = Some(layer);
self
}
#[cfg(feature = "flamegraph")]
pub fn with_flamegraph(mut self) -> Self {
self.enable_flamegraph = true;
self
}
#[cfg(feature = "colored")]
pub fn with_colors(mut self) -> Self {
self.builder = self.builder.with_ansi(true);
self.use_colors = true;
self
}
pub fn with_file_logging(self) -> Self {
self.target(Target::LogDir { file_name: None })
}
pub fn with_rotation(mut self, rotation: Rotation) -> Self {
self.rotation = rotation;
self
}
pub fn with_rotation_strategy(mut self, strategy: RotationStrategy) -> Self {
self.rotation_strategy = strategy;
self
}
pub fn with_max_file_size(mut self, size: MaxFileSize) -> Self {
self.max_file_size = Some(size);
self
}
pub fn with_timezone_strategy(mut self, strategy: TimezoneStrategy) -> Self {
self.timezone_strategy = strategy;
self
}
pub fn with_format(mut self, format: LogFormat) -> Self {
self.log_format = format;
self
}
pub fn with_file(mut self, show: bool) -> Self {
self.show_file = show;
self
}
pub fn with_line_number(mut self, show: bool) -> Self {
self.show_line_number = show;
self
}
pub fn with_thread_ids(mut self, show: bool) -> Self {
self.show_thread_ids = show;
self
}
pub fn with_thread_names(mut self, show: bool) -> Self {
self.show_thread_names = show;
self
}
pub fn with_target_display(mut self, show: bool) -> Self {
self.show_target = show;
self
}
pub fn with_level(mut self, show: bool) -> Self {
self.show_level = show;
self
}
pub fn target(mut self, target: Target) -> Self {
self.targets.push(target);
self
}
pub fn targets(mut self, targets: impl IntoIterator<Item = Target>) -> Self {
self.targets = targets.into_iter().collect();
self
}
pub fn clear_targets(mut self) -> Self {
self.targets.clear();
self
}
pub fn with_default_subscriber(mut self) -> Self {
self.set_default_subscriber = true;
self
}
pub fn configured_targets(&self) -> &[Target] {
&self.targets
}
pub fn configured_rotation(&self) -> Rotation {
self.rotation
}
pub fn configured_rotation_strategy(&self) -> RotationStrategy {
self.rotation_strategy
}
pub fn configured_max_file_size(&self) -> Option<MaxFileSize> {
self.max_file_size
}
pub fn configured_timezone_strategy(&self) -> TimezoneStrategy {
self.timezone_strategy
}
pub fn configured_format(&self) -> LogFormat {
self.log_format
}
pub fn configured_format_options(&self) -> FormatOptions {
FormatOptions {
format: self.log_format,
file: self.show_file,
line_number: self.show_line_number,
thread_ids: self.show_thread_ids,
thread_names: self.show_thread_names,
target: self.show_target,
level: self.show_level,
}
}
pub fn build_filter(&self) -> Targets {
self.filter.clone().with_default(self.log_level)
}
#[cfg(feature = "flamegraph")]
fn plugin_builder<R: Runtime>() -> plugin::Builder<R> {
plugin::Builder::new("tracing").invoke_handler(tauri::generate_handler![
commands::log,
commands::generate_flamegraph,
commands::generate_flamechart
])
}
#[cfg(not(feature = "flamegraph"))]
fn plugin_builder<R: Runtime>() -> plugin::Builder<R> {
plugin::Builder::new("tracing").invoke_handler(tauri::generate_handler![commands::log,])
}
pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
let log_level = self.log_level;
let filter = self.filter;
let custom_filter = self.custom_filter;
let custom_layer = self.custom_layer;
let targets = self.targets;
let rotation = self.rotation;
let rotation_strategy = self.rotation_strategy;
let max_file_size = self.max_file_size;
let timezone_strategy = self.timezone_strategy;
let format_options = FormatOptions {
format: self.log_format,
file: self.show_file,
line_number: self.show_line_number,
thread_ids: self.show_thread_ids,
thread_names: self.show_thread_names,
target: self.show_target,
level: self.show_level,
};
let set_default_subscriber = self.set_default_subscriber;
#[cfg(feature = "colored")]
let use_colors = self.use_colors;
#[cfg(feature = "flamegraph")]
let enable_flamegraph = self.enable_flamegraph;
Self::plugin_builder()
.setup(move |app, _api| {
#[cfg(feature = "flamegraph")]
setup_flamegraph(app);
#[cfg(desktop)]
if set_default_subscriber {
let guard = acquire_logger(
app,
log_level,
filter,
custom_filter,
custom_layer,
&targets,
rotation,
rotation_strategy,
max_file_size,
timezone_strategy,
format_options,
#[cfg(feature = "colored")]
use_colors,
#[cfg(feature = "flamegraph")]
enable_flamegraph,
)?;
if guard.is_some() {
app.manage(LogGuard(guard));
}
}
Ok(())
})
.build()
}
}
struct FileTargetConfig {
log_dir: PathBuf,
file_name: String,
}
fn resolve_file_target<R: Runtime>(
app_handle: &AppHandle<R>,
target: &Target,
) -> Result<Option<FileTargetConfig>> {
match target {
Target::LogDir { file_name } => {
let log_dir = app_handle.path().app_log_dir()?;
std::fs::create_dir_all(&log_dir)?;
Ok(Some(FileTargetConfig {
log_dir,
file_name: file_name.clone().unwrap_or_else(|| "app".to_string()),
}))
}
Target::Folder { path, file_name } => {
std::fs::create_dir_all(path)?;
Ok(Some(FileTargetConfig {
log_dir: path.clone(),
file_name: file_name.clone().unwrap_or_else(|| "app".to_string()),
}))
}
_ => Ok(None),
}
}
fn cleanup_old_logs(
log_dir: &std::path::Path,
file_prefix: &str,
strategy: RotationStrategy,
) -> Result<()> {
match strategy {
RotationStrategy::KeepAll => Ok(()),
RotationStrategy::KeepOne => cleanup_logs_keeping(log_dir, file_prefix, 1),
RotationStrategy::KeepSome(n) => cleanup_logs_keeping(log_dir, file_prefix, n as usize),
}
}
fn cleanup_logs_keeping(log_dir: &std::path::Path, file_prefix: &str, keep: usize) -> Result<()> {
let prefix_with_dot = format!("{}.", file_prefix);
let mut log_files: Vec<_> = std::fs::read_dir(log_dir)?
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry
.file_name()
.to_str()
.is_some_and(|name| name.starts_with(&prefix_with_dot) && name.ends_with(".log"))
})
.collect();
log_files.sort_by_key(|entry| std::cmp::Reverse(entry.file_name()));
for entry in log_files.into_iter().skip(keep) {
if let Err(e) = std::fs::remove_file(entry.path()) {
tracing::warn!("Failed to remove old log file {:?}: {}", entry.path(), e);
}
}
Ok(())
}
#[cfg(desktop)]
#[allow(clippy::too_many_arguments)]
fn acquire_logger<R: Runtime>(
app_handle: &AppHandle<R>,
log_level: LevelFilter,
filter: Targets,
custom_filter: Option<FilterFn>,
custom_layer: Option<BoxedLayer>,
targets: &[Target],
rotation: Rotation,
rotation_strategy: RotationStrategy,
max_file_size: Option<MaxFileSize>,
timezone_strategy: TimezoneStrategy,
format_options: FormatOptions,
#[cfg(feature = "colored")] use_colors: bool,
#[cfg(feature = "flamegraph")] enable_flamegraph: bool,
) -> Result<Option<WorkerGuard>> {
use std::io;
use tracing_subscriber::fmt::time::OffsetTime;
let filter_with_default = filter.with_default(log_level);
let has_stdout = targets.iter().any(|t| matches!(t, Target::Stdout));
let has_stderr = targets.iter().any(|t| matches!(t, Target::Stderr));
let has_webview = targets.iter().any(|t| matches!(t, Target::Webview));
let file_config = targets
.iter()
.find_map(|t| resolve_file_target(app_handle, t).transpose())
.transpose()?;
#[cfg(feature = "colored")]
let use_ansi = use_colors;
#[cfg(not(feature = "colored"))]
let use_ansi = false;
let make_timer = || match timezone_strategy {
TimezoneStrategy::Utc => OffsetTime::new(
time::UtcOffset::UTC,
time::format_description::well_known::Rfc3339,
),
TimezoneStrategy::Local => time::UtcOffset::current_local_offset()
.map(|offset| OffsetTime::new(offset, time::format_description::well_known::Rfc3339))
.unwrap_or_else(|_| {
OffsetTime::new(
time::UtcOffset::UTC,
time::format_description::well_known::Rfc3339,
)
}),
};
macro_rules! make_layer {
($layer:expr, $format:expr) => {
match $format {
LogFormat::Full => $layer.boxed(),
LogFormat::Compact => $layer.compact().boxed(),
LogFormat::Pretty => $layer.pretty().boxed(),
}
};
}
let stdout_layer = if has_stdout {
let layer = fmt::layer()
.with_timer(make_timer())
.with_ansi(use_ansi)
.with_file(format_options.file)
.with_line_number(format_options.line_number)
.with_thread_ids(format_options.thread_ids)
.with_thread_names(format_options.thread_names)
.with_target(format_options.target)
.with_level(format_options.level);
Some(make_layer!(layer, format_options.format))
} else {
None
};
let stderr_layer = if has_stderr {
let layer = fmt::layer()
.with_timer(make_timer())
.with_ansi(use_ansi)
.with_file(format_options.file)
.with_line_number(format_options.line_number)
.with_thread_ids(format_options.thread_ids)
.with_thread_names(format_options.thread_names)
.with_target(format_options.target)
.with_level(format_options.level)
.with_writer(io::stderr);
Some(make_layer!(layer, format_options.format))
} else {
None
};
let webview_layer = if has_webview {
Some(WebviewLayer::new(app_handle.clone()))
} else {
None
};
let (file_layer, guard) = if let Some(config) = file_config {
if max_file_size.is_none() {
cleanup_old_logs(&config.log_dir, &config.file_name, rotation_strategy)?;
}
if let Some(max_size) = max_file_size {
use rolling_file::{BasicRollingFileAppender, RollingConditionBasic};
let mut condition = RollingConditionBasic::new();
condition = match rotation {
Rotation::Daily => condition.daily(),
Rotation::Hourly => condition.hourly(),
Rotation::Minutely => condition, Rotation::Never => condition, };
condition = condition.max_size(max_size.0);
let max_files = match rotation_strategy {
RotationStrategy::KeepAll => u32::MAX as usize,
RotationStrategy::KeepOne => 1,
RotationStrategy::KeepSome(n) => n as usize,
};
let log_path = config.log_dir.join(format!("{}.log", config.file_name));
let file_appender = BasicRollingFileAppender::new(log_path, condition, max_files)
.map_err(std::io::Error::other)?;
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let strip_ansi_writer = StripAnsiWriter::new(non_blocking);
let layer = fmt::layer()
.with_timer(make_timer())
.with_ansi(false)
.with_file(format_options.file)
.with_line_number(format_options.line_number)
.with_thread_ids(format_options.thread_ids)
.with_thread_names(format_options.thread_names)
.with_target(format_options.target)
.with_level(format_options.level)
.with_writer(strip_ansi_writer);
(Some(make_layer!(layer, format_options.format)), Some(guard))
} else {
use tracing_appender::rolling::RollingFileAppender;
let appender_rotation = match rotation {
Rotation::Daily => tracing_appender::rolling::Rotation::DAILY,
Rotation::Hourly => tracing_appender::rolling::Rotation::HOURLY,
Rotation::Minutely => tracing_appender::rolling::Rotation::MINUTELY,
Rotation::Never => tracing_appender::rolling::Rotation::NEVER,
};
let file_appender = RollingFileAppender::builder()
.rotation(appender_rotation)
.filename_prefix(&config.file_name)
.filename_suffix("log")
.build(&config.log_dir)
.map_err(std::io::Error::other)?;
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let strip_ansi_writer = StripAnsiWriter::new(non_blocking);
let layer = fmt::layer()
.with_timer(make_timer())
.with_ansi(false)
.with_file(format_options.file)
.with_line_number(format_options.line_number)
.with_thread_ids(format_options.thread_ids)
.with_thread_names(format_options.thread_names)
.with_target(format_options.target)
.with_level(format_options.level)
.with_writer(strip_ansi_writer);
(Some(make_layer!(layer, format_options.format)), Some(guard))
}
} else {
(None, None)
};
#[cfg(feature = "flamegraph")]
let flame_layer = if enable_flamegraph {
Some(create_flame_layer(app_handle)?)
} else {
None
};
let custom_filter_layer = custom_filter.map(|f| filter_fn(move |metadata| f(metadata)));
#[cfg(feature = "flamegraph")]
let combined_boxed_layer: Option<BoxedLayer> = match (custom_layer, flame_layer) {
(Some(c), Some(f)) => {
use tracing_subscriber::Layer;
Some(c.and_then(f).boxed())
}
(Some(c), None) => Some(c),
(None, Some(f)) => Some(f),
(None, None) => None,
};
#[cfg(not(feature = "flamegraph"))]
let combined_boxed_layer = custom_layer;
let subscriber = Registry::default()
.with(combined_boxed_layer)
.with(stdout_layer)
.with(stderr_layer)
.with(file_layer)
.with(webview_layer)
.with(custom_filter_layer)
.with(filter_with_default);
tracing::subscriber::set_global_default(subscriber)?;
tracing::info!("tracing initialized");
Ok(guard)
}