bevy telemetry integration
Guide for bevy telemetry integration
Bevy Telemetry Integration
This document explains how to integrate custom telemetry (Prometheus, Sentry, tracing) with Bevy's plugin system.
The Problem
Bevy's
DefaultPlugins includes LogPlugin, which initializes tracing-subscriber automatically. If you want to use custom telemetry with additional layers (Sentry, Prometheus), you'll encounter a conflict:failed to set global default subscriber: SetGlobalDefaultError("a global default trace dispatcher has already been set")The Solution
Disable Bevy's
LogPlugin and initialize telemetry yourself via a custom plugin.Implementation
Step 1: Create TelemetryPlugin
File:
src/systems/telemetry.rsuse bevy::prelude::*;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use metrics_exporter_prometheus::PrometheusBuilder;
pub struct TelemetryPlugin;
impl Plugin for TelemetryPlugin {
fn build(&self, _app: &mut App) {
init_telemetry();
}
}
pub fn init_telemetry() {
// 1. Load environment variables
dotenv::dotenv().ok();
// 2. Initialize Sentry (error tracking)
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
};
// 3. Initialize tracing-subscriber 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(); // ← Single initialization point
// 4. Initialize Prometheus metrics exporter
let builder = PrometheusBuilder::new();
builder
.with_http_listener(([0, 0, 0, 0], 9000))
.install()
.expect("failed to install Prometheus recorder");
info!("Telemetry initialized. Metrics at http://localhost:9000/metrics");
}Step 2: Disable LogPlugin in Main App
File:
src/lib.rsuse bevy::prelude::*;
use bevy_egui::EguiPlugin;
pub fn run() {
let mut app = App::new();
// Add DefaultPlugins but DISABLE LogPlugin
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>() // ← Critical: Disable Bevy's logger
)
.add_plugins(EguiPlugin);
// Add custom telemetry BEFORE game systems
app.add_plugins(TelemetryPlugin)
.add_plugins(GamePlugins)
.run();
}Step 3: Register TelemetryPlugin Module
File:
src/systems/mod.rspub mod telemetry;Plugin Ordering
Important: Initialize
TelemetryPlugin before game systems that might log:app
.add_plugins(DefaultPlugins.disable::<bevy::log::LogPlugin>())
.add_plugins(TelemetryPlugin) // ← First: Initialize telemetry
.add_plugins(CameraPlugin) // ← Then: Game systems can log
.add_plugins(WorldPlugin)
.add_plugins(PlayerPlugin)
// ...Using Telemetry in Game Systems
Logging with Tracing
use bevy::prelude::*;
pub fn my_game_system(query: Query<&Transform>) {
info!("System started");
for (i, transform) in query.iter().enumerate() {
debug!(index = i, position = ?transform.translation, "Processing entity");
}
info!(entity_count = query.iter().count(), "System completed");
}Recording Metrics
use bevy::prelude::*;
use metrics::{counter, histogram, gauge};
pub fn tick_metrics_system(
time: Res<Time>,
entities: Query<Entity>,
) {
// Record tick duration
histogram!("tick_duration_seconds").record(time.delta_seconds_f64());
// Record entity count
gauge!("entities_count").set(entities.iter().count() as f64);
// Increment event counter
counter!("game_ticks_total").increment(1);
}Capturing Errors to Sentry
use bevy::prelude::*;
pub fn error_prone_system() {
match risky_operation() {
Ok(result) => info!("Success: {:?}", result),
Err(e) => {
error!("Operation failed: {}", e); // ← Automatically sent to Sentry
sentry::capture_error(&e); // ← Explicit capture with context
}
}
}Headless Mode Support
For headless servers (no rendering), use
MinimalPlugins instead:pub fn run() {
let headless = std::env::var("HEADLESS_MODE")
.unwrap_or_default()
.to_lowercase() == "true";
let mut app = App::new();
if headless {
// Headless: MinimalPlugins doesn't include LogPlugin
app.add_plugins(MinimalPlugins)
.add_plugins(bevy::asset::AssetPlugin::default());
} else {
// Normal: Disable LogPlugin in DefaultPlugins
app.add_plugins(DefaultPlugins.disable::<bevy::log::LogPlugin>())
.add_plugins(EguiPlugin);
}
app.add_plugins(TelemetryPlugin)
.add_plugins(GamePlugins)
.run();
}Environment Variables
Configure telemetry via environment variables:
# Logging level
export RUST_LOG=info,legends_client=debug,bevy_render=warn
# Sentry error tracking
export SENTRY_DSN=https://<key>@sentry.io/<project>
# Run the game
cargo run --bin legends_clientDependencies
File:
Cargo.toml[dependencies]
# Bevy
bevy = { version = "0.14", features = ["default"] }
# Observability
sentry = { version = "0.36", features = ["tracing"] }
sentry-tracing = "0.36"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tracing-appender = "0.2"
tracing-log = "0.2"
metrics = "0.24"
metrics-exporter-prometheus = "0.16"
dotenv = "0.15"Troubleshooting
Still getting double-initialization error?
Check:
LogPluginis disabled:.disable::<bevy::log::LogPlugin>()- No other code calls
tracing_subscriber::init()or.try_init() TelemetryPluginis added before other plugins that might initialize tracing
Metrics endpoint not working?
Verify:
curl http://localhost:9000/metricsCheck that
PrometheusBuilder installed successfully (no panic in logs).Sentry not receiving errors?
Verify:
SENTRY_DSNenvironment variable is set- Sentry layer is added:
.with(sentry_layer) - Errors are logged at
ERRORlevel or explicitly captured withsentry::capture_error()
Related Documentation
- tracing_panic_fix.md: Detailed explanation of the panic
- sre_architecture.md: Overall observability strategy