tracing panic fix

Guide for tracing panic fix

Tracing-Subscriber Double-Initialization Panic Fix

Problem

When running the Legends of Hastinapur game client, it crashes immediately at startup with:
thread 'main' (19072) panicked at /home/ecom-nithinm/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-subscriber-0.3.22/src/util.rs:94:14:
failed to set global default subscriber: SetGlobalDefaultError("a global default trace dispatcher has already been set")

Root Cause

Conflict between two tracing initializations:
  1. Bevy's LogPlugin (part of DefaultPlugins):
    • Automatically initializes tracing-subscriber for logging
    • Calls .init() on the global subscriber
  2. Custom TelemetryPlugin (in src/systems/telemetry.rs):
    • Also initializes tracing-subscriber with custom configuration
    • Calls .init() on the global subscriber (line 49)
Result: The second .init() call fails because Rust's tracing-subscriber only allows one global subscriber per process.

Solution

Disable Bevy's LogPlugin when using custom telemetry initialization.

Code Change

File: src/lib.rs
Before:
app.add_plugins(DefaultPlugins.set(WindowPlugin {
    primary_window: Some(Window {
        title: "Legends of Hastinapur".into(),
        resolution: (1280., 720.).into(),
        ..default()
    }),
    ..default()
}))
After:
app.add_plugins(DefaultPlugins.set(WindowPlugin {
    primary_window: Some(Window {
        title: "Legends of Hastinapur".into(),
        resolution: (1280., 720.).into(),
        ..default()
    }),
    ..default()
}).disable::<bevy::log::LogPlugin>())

Why This Works

  • DefaultPlugins is a plugin group that includes LogPlugin
  • .disable::<bevy::log::LogPlugin>() removes LogPlugin from the group
  • This prevents Bevy from initializing its own tracing subscriber
  • Our custom TelemetryPlugin can now safely initialize the global subscriber

Custom Telemetry Setup

File: src/systems/telemetry.rs
Our custom telemetry provides:
  • Structured logging with tracing-subscriber
  • Prometheus metrics on http://localhost:9000/metrics
  • Sentry error tracking (configured via SENTRY_DSN env var)
pub fn init_telemetry() {
    // Initialize Sentry
    let sentry_dsn = std::env::var("SENTRY_DSN").unwrap_or_default();
    let _guard = if !sentry_dsn.is_empty() {
        Some(sentry::init((sentry_dsn, sentry::ClientOptions {
            release: sentry::release_name!(),
            ..Default::default()
        })))
    } else {
        None
    };

    // Initialize Tracing with custom layers
    let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| "info,legends_client=debug".into());

    let fmt_layer = tracing_subscriber::fmt::layer()
        .with_target(false)
        .with_thread_ids(true)
        .with_level(true);

    let sentry_layer = sentry_tracing::layer();

    tracing_subscriber::registry()
        .with(env_filter)
        .with(fmt_layer)
        .with(sentry_layer)
        .init(); // ← This is the ONLY .init() call now

    // Initialize Prometheus metrics
    let builder = PrometheusBuilder::new();
    builder
        .with_http_listener(([0, 0, 0, 0], 9000))
        .install()
        .expect("failed to install Prometheus recorder");
}

Verification

After applying the fix:
cargo run --bin legends_client
Expected output (no panic):
warning: `legends_client` (lib) generated 17 warnings
    Finished `dev` profile [optimized + debuginfo] target(s) in 24.74s
     Running `target/debug/legends_client`
2026-01-05T15:04:15.024388Z  INFO ThreadId(01) Telemetry initialized. Metrics at http://localhost:9000/metrics
2026-01-05T15:04:15.025050Z  INFO ThreadId(01) No plugin public key found at secrets/plugin_pub.key. Signature verification will be skipped (DEV) or fail (PROD).
2026-01-05T15:04:15.049447Z  INFO ThreadId(01) Creating new window "Legends of Hastinapur" (Entity { index: 0, generation: 1 })
Verification Status: ✅ CONFIRMED - The game launches successfully without the tracing panic.

Original Expected Output

2026-01-05T14:59:22.476265Z  INFO bevy_diagnostic::system_information_diagnostics_plugin::internal: SystemInfo { os: "Linux 24.04 Ubuntu", kernel: "6.8.0-49-generic", cpu: "Intel(R) Core(TM) Ultra 9 185H", core_count: "16", memory: "30.8 GiB" }
2026-01-05T14:59:22.634362Z  INFO bevy_render::renderer: AdapterInfo { name: "NVIDIA RTX 3000 Ada Generation Laptop GPU", ... }
Telemetry initialized. Metrics at http://localhost:9000/metrics

General Pattern for Bevy + Custom Telemetry

When integrating custom observability tools with Bevy:
  1. Always disable LogPlugin if you're initializing tracing-subscriber yourself
  2. Initialize telemetry early in the plugin chain (before game systems)
  3. Use TelemetryPlugin::build() to ensure initialization happens during app setup
app
    .add_plugins(DefaultPlugins.disable::<bevy::log::LogPlugin>())
    .add_plugins(TelemetryPlugin) // Custom telemetry first
    .add_plugins(GamePlugins)      // Then game systems
  • Headless mode: If running in headless mode (no rendering), use MinimalPlugins instead of DefaultPlugins to avoid this issue entirely
  • Multiple binaries: If you have multiple binaries (client, server, editor), ensure each disables LogPlugin if using custom telemetry

Dependencies

[dependencies]
sentry = { version = "0.36", features = ["tracing"] }
sentry-tracing = "0.36"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
metrics = "0.24"
metrics-exporter-prometheus = "0.16"