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

  1. Ensure logs::init runs exactly once per process, at app startup
  2. Treat a failed install as non-fatal: log a warning and keep the existing subscriber (see exampleFix)
  3. In tests, use thread-local set_default/with_default instead of global installation
  4. 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

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


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/6d3ba0cd7d7a08c8. Report an issue: GitHub.