bevy_fleet 0.1.0

bevy swarm diagnostic, event, metric, and telemetry client
Documentation
//! # bevy_fleet
//!
//! A Bevy plugin that asynchronously publishes diagnostics, events, panics, machine info,
//! and custom metrics to a configurable cloud endpoint.
//!
//! This plugin integrates with Bevy's built-in diagnostic system to collect
//! telemetry data without custom interfaces.
//!
//! ## Features
//!
//! - Async (off-game-thread) publishing
//! - Integration with Bevy's diagnostic system
//! - Panic capture and reporting
//! - Machine/system information
//! - Configurable cloud endpoint
//!
//! ## Usage
//!
//! ```rust,no_run
//! use bevy::prelude::*;
//! use bevy::diagnostic::FrameTimeDiagnosticsPlugin;
//! use bevy_fleet::{FleetPlugin, FleetConfig};
//!
//! App::new()
//!     .add_plugins(DefaultPlugins)
//!     .add_plugins(FrameTimeDiagnosticsPlugin::default())
//!     .add_plugins(FleetPlugin {
//!         config: FleetConfig {
//!             app_id: "my-app".to_string(),
//!             aggregation_url: "https://fleet.example.com/api/v1/telemetry".to_string(),
//!             enabled: true,
//!             publish_interval_secs: 60,
//!             ..Default::default()
//!         },
//!     })
//!     .run();
//! ```

mod config;
mod diagnostics;
mod events;
mod machine_info;
mod metrics;
mod otlp;
mod panic_handler;
mod publisher;
mod session;

pub use config::{FleetConfig, FleetPlugin, OtlpConfig};
pub use diagnostics::FleetDiagnostic;
pub use events::{FleetEvent, FleetEventBuffer, forward_serialized_events};
pub use machine_info::{GpuInfo, MachineInfo};
pub use metrics::FleetMetric;
pub use panic_handler::PanicInfo;
pub use session::{SessionInfo, SessionStats};

use bevy::prelude::*;

/// System to setup panic handler with session ID and flush callback
fn setup_panic_handler_with_session(
    session_info: Res<SessionInfo>,
    sender_res: Res<publisher::TelemetrySender>,
    config: Res<FleetConfig>,
    machine_info: Option<Res<MachineInfo>>,
) {
    panic_handler::setup_panic_handler();
    panic_handler::set_session_id(session_info.session_id.clone());

    // Clone resources needed for panic flush
    #[cfg(not(target_arch = "wasm32"))]
    let panic_sender = sender_res.sender.clone();
    #[cfg(target_arch = "wasm32")]
    let panic_sender = sender_res.clone();
    let session_id = session_info.session_id.clone();
    let session_start_time = session_info.session_start_time;
    let session_stats_base = session_info.stats.clone();
    let app_id = config.app_id.clone();
    let machine_info_clone = machine_info.map(|info| (*info).clone());

    // Set up panic flush callback that immediately sends telemetry
    panic_handler::set_panic_flush_callback(move || {
        // Collect panic information
        let panics = panic_handler::get_panics();
        if panics.is_empty() {
            return;
        }

        // Create updated session stats with panic count
        let mut session_stats = session_stats_base.clone();
        session_stats.panics_captured += panics.len() as u64;

        // Create immediate telemetry payload
        let payload = publisher::TelemetryPayload {
            app_id: app_id.clone(),
            timestamp: std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs(),
            session_id: session_id.clone(),
            session_start_time,
            session_stats,
            machine_info: machine_info_clone.clone(),
            metrics: vec![],
            events: vec![],
            diagnostics: vec![],
            panics,
        };

        // Try to send immediately (blocking)
        // Use a blocking send with timeout to ensure panic data is sent
        #[cfg(not(target_arch = "wasm32"))]
        {
            if let Err(err) = panic_sender.blocking_send(payload) {
                eprintln!("Fleet: Failed to send panic telemetry: {}", err);
            }
        }

        #[cfg(target_arch = "wasm32")]
        {
            if let Err(err) = panic_sender.send_now(payload) {
                eprintln!("Fleet: Failed to send panic telemetry: {}", err);
            }
        }
    });

    info!(
        target: "bevy_fleet",
        "Fleet telemetry enabled for session {}",
        session_info.session_id
    );
}

/// Registers the Fleet plugin with Bevy
impl Plugin for FleetPlugin {
    fn build(&self, app: &mut App) {
        if !self.config.enabled {
            info!(target: "bevy_fleet", "Fleet plugin is disabled");
            return;
        }

        // Initialize session info
        session::initialize_session(app);

        // Register Fleet events and tracing bridge
        app.insert_resource(self.config.clone())
            .insert_resource(events::FleetEventBuffer::default())
            .add_message::<FleetEvent>()
            .add_systems(Startup, publisher::setup_publisher)
            // Set up panic handler after publisher is initialized
            .add_systems(
                Startup,
                setup_panic_handler_with_session.after(publisher::setup_publisher),
            )
            .add_systems(
                PostUpdate,
                (events::collect_fleet_events, publisher::publish_telemetry).chain(),
            )
            .add_systems(Update, publisher::log_publisher_stats);

        // Collect initial machine info
        machine_info::collect_machine_info(app);
    }
}