bevy_fleet/
lib.rs

1//! # bevy_fleet
2//!
3//! A Bevy plugin that asynchronously publishes diagnostics, events, panics, machine info,
4//! and custom metrics to a configurable cloud endpoint.
5//!
6//! This plugin integrates with Bevy's built-in diagnostic system to collect
7//! telemetry data without custom interfaces.
8//!
9//! ## Features
10//!
11//! - Async (off-game-thread) publishing
12//! - Integration with Bevy's diagnostic system
13//! - Panic capture and reporting
14//! - Machine/system information
15//! - Configurable cloud endpoint
16//!
17//! ## Usage
18//!
19//! ```rust,no_run
20//! use bevy::prelude::*;
21//! use bevy::diagnostic::FrameTimeDiagnosticsPlugin;
22//! use bevy_fleet::{FleetPlugin, FleetConfig};
23//!
24//! App::new()
25//!     .add_plugins(DefaultPlugins)
26//!     .add_plugins(FrameTimeDiagnosticsPlugin::default())
27//!     .add_plugins(FleetPlugin {
28//!         config: FleetConfig {
29//!             app_id: "my-app".to_string(),
30//!             aggregation_url: "https://fleet.example.com/api/v1/telemetry".to_string(),
31//!             enabled: true,
32//!             publish_interval_secs: 60,
33//!             ..Default::default()
34//!         },
35//!     })
36//!     .run();
37//! ```
38
39mod config;
40mod diagnostics;
41mod events;
42mod machine_info;
43mod metrics;
44mod otlp;
45mod panic_handler;
46mod publisher;
47mod session;
48
49pub use config::{FleetConfig, FleetPlugin, OtlpConfig};
50pub use diagnostics::FleetDiagnostic;
51pub use events::{FleetEvent, FleetEventBuffer, forward_serialized_events};
52pub use machine_info::{GpuInfo, MachineInfo};
53pub use metrics::FleetMetric;
54pub use panic_handler::PanicInfo;
55pub use session::{SessionInfo, SessionStats};
56
57use bevy::prelude::*;
58
59/// System to setup panic handler with session ID and flush callback
60fn setup_panic_handler_with_session(
61    session_info: Res<SessionInfo>,
62    sender_res: Res<publisher::TelemetrySender>,
63    config: Res<FleetConfig>,
64    machine_info: Option<Res<MachineInfo>>,
65) {
66    panic_handler::setup_panic_handler();
67    panic_handler::set_session_id(session_info.session_id.clone());
68
69    // Clone resources needed for panic flush
70    #[cfg(not(target_arch = "wasm32"))]
71    let panic_sender = sender_res.sender.clone();
72    #[cfg(target_arch = "wasm32")]
73    let panic_sender = sender_res.clone();
74    let session_id = session_info.session_id.clone();
75    let session_start_time = session_info.session_start_time;
76    let session_stats_base = session_info.stats.clone();
77    let app_id = config.app_id.clone();
78    let machine_info_clone = machine_info.map(|info| (*info).clone());
79
80    // Set up panic flush callback that immediately sends telemetry
81    panic_handler::set_panic_flush_callback(move || {
82        // Collect panic information
83        let panics = panic_handler::get_panics();
84        if panics.is_empty() {
85            return;
86        }
87
88        // Create updated session stats with panic count
89        let mut session_stats = session_stats_base.clone();
90        session_stats.panics_captured += panics.len() as u64;
91
92        // Create immediate telemetry payload
93        let payload = publisher::TelemetryPayload {
94            app_id: app_id.clone(),
95            timestamp: std::time::SystemTime::now()
96                .duration_since(std::time::UNIX_EPOCH)
97                .unwrap_or_default()
98                .as_secs(),
99            session_id: session_id.clone(),
100            session_start_time,
101            session_stats,
102            machine_info: machine_info_clone.clone(),
103            metrics: vec![],
104            events: vec![],
105            diagnostics: vec![],
106            panics,
107        };
108
109        // Try to send immediately (blocking)
110        // Use a blocking send with timeout to ensure panic data is sent
111        #[cfg(not(target_arch = "wasm32"))]
112        {
113            if let Err(err) = panic_sender.blocking_send(payload) {
114                eprintln!("Fleet: Failed to send panic telemetry: {}", err);
115            }
116        }
117
118        #[cfg(target_arch = "wasm32")]
119        {
120            if let Err(err) = panic_sender.send_now(payload) {
121                eprintln!("Fleet: Failed to send panic telemetry: {}", err);
122            }
123        }
124    });
125
126    info!(
127        target: "bevy_fleet",
128        "Fleet telemetry enabled for session {}",
129        session_info.session_id
130    );
131}
132
133/// Registers the Fleet plugin with Bevy
134impl Plugin for FleetPlugin {
135    fn build(&self, app: &mut App) {
136        if !self.config.enabled {
137            info!(target: "bevy_fleet", "Fleet plugin is disabled");
138            return;
139        }
140
141        // Initialize session info
142        session::initialize_session(app);
143
144        // Register Fleet events and tracing bridge
145        app.insert_resource(self.config.clone())
146            .insert_resource(events::FleetEventBuffer::default())
147            .add_message::<FleetEvent>()
148            .add_systems(Startup, publisher::setup_publisher)
149            // Set up panic handler after publisher is initialized
150            .add_systems(
151                Startup,
152                setup_panic_handler_with_session.after(publisher::setup_publisher),
153            )
154            .add_systems(
155                PostUpdate,
156                (events::collect_fleet_events, publisher::publish_telemetry).chain(),
157            )
158            .add_systems(Update, publisher::log_publisher_stats);
159
160        // Collect initial machine info
161        machine_info::collect_machine_info(app);
162    }
163}