gitbutlerapp/gitbutler · critical
failed to set subscriber
Error message
failed to set subscriber
What it means
logs::init() finishes by installing a global tracing subscriber via tracing::subscriber::set_global_default(...).expect("failed to set subscriber"). set_global_default returns Err precisely when a global default subscriber is already installed - a process allows exactly one. The panic therefore means logs::init ran twice, or another component claimed the global subscriber first (e.g. tracing_subscriber::fmt().init() in a dependency or test harness).
Source
Thrown at crates/gitbutler-tauri/src/logs.rs:90
tracing_forest::ForestLayer::from(
tracing_forest::printer::PrettyPrinter::new().writer(std::io::stdout),
)
.with_filter(filter_fn(move |meta| should_log(log_level, meta))),
),
)
} else {
set_global_default(
subscriber.with(
// subscriber that writes spans to stdout
tracing_subscriber::fmt::layer()
.event_format(format_for_humans)
.with_ansi(use_colors_in_logs)
.with_span_events(FmtSpan::CLOSE)
.with_filter(filter_fn(move |meta| should_log(log_level, meta))),
),
)
}
.expect("failed to set subscriber");
}
/// This function is much like `LevelFilter`, but it also filters based on the module path.
/// This is necessary, strangely enough, in release builds only, but is the same in the `but` CLI builds as well.
/// It is pretty much the same as `LevelFilter` in debug builds.
fn should_log(level: Option<Level>, meta: &tracing::Metadata<'_>) -> bool {
let Some(level) = level else {
return false;
};
if *meta.level() > level {
return false;
}
if level > Level::DEBUG {
return true;
}
meta.module_path().is_none_or(|p| {
p.starts_with("gitbutler_") || p.starts_with("but::") || p.starts_with("but_")
})View on GitHub (pinned to caf1f223d3)
Solutions
- Ensure logs::init runs exactly once per process, at app startup
- Treat a failed install as non-fatal: log a warning and keep the existing subscriber (see exampleFix)
- In tests, use thread-local set_default/with_default instead of global installation
- If a plugin also configures logging (e.g. tauri_plugin_log), order initialization so logs::init runs first or drop one of the two
Example fix
// before
set_global_default(subscriber).expect("failed to set subscriber");
// after
if set_global_default(subscriber).is_err() {
eprintln!("warning: global tracing subscriber already set; skipping re-init");
} Defensive patterns
Strategy: validation
Validate before calling
// One-shot guard around logging init at the host boundary
static LOGS_INIT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if LOGS_INIT.swap(true, std::sync::atomic::Ordering::SeqCst) {
return; // already initialized - a second set_global_default would panic
} Prevention
- Install the global tracing subscriber exactly once per process
- Use thread-local tracing::subscriber::set_default in tests, never fmt().init() in libraries
- Order plugin logging vs logs::init deliberately; check for duplicate setup entry points
When it happens
Trigger: Calling logs::init twice (double setup, retry/re-entry logic), or initializing tracing elsewhere first: a dependency calling tracing_subscriber::fmt().init(), a test harness with its own subscriber, or another logging plugin registering a global default before the Tauri setup hook runs.
Common situations: E2E test harnesses that install their own subscriber before launching the app, refactors adding early logging, or duplicated initialization after an error-retry path re-enters setup.
Related errors
- theme::init() must be called before getting the theme
- initializing rolling file appender failed
- broker already configured
- failed to create app settings
- theme may only be initialized once
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/6d3ba0cd7d7a08c8.
Report an issue: GitHub.