Skip to main content

ferro_rs/debug/
mod.rs

1//! Debug introspection endpoints for development
2//!
3//! These endpoints expose runtime application state for AI-assisted development
4//! and debugging. They are automatically disabled in production.
5
6use crate::config::Config;
7use crate::container::get_registered_services;
8use crate::metrics;
9use crate::middleware::get_global_middleware_info;
10use crate::routing::get_registered_routes;
11use bytes::Bytes;
12use chrono::Utc;
13use http_body_util::Full;
14use serde::Serialize;
15
16/// Response wrapper for debug endpoints
17#[derive(Debug, Serialize)]
18pub struct DebugResponse<T: Serialize> {
19    /// Whether the debug operation succeeded.
20    pub success: bool,
21    /// The debug payload.
22    pub data: T,
23    /// RFC 3339 timestamp of when the response was generated.
24    pub timestamp: String,
25}
26
27/// Error response for debug endpoints
28#[derive(Debug, Serialize)]
29pub struct DebugErrorResponse {
30    /// Always `false` for error responses.
31    pub success: bool,
32    /// Human-readable error description.
33    pub error: String,
34    /// RFC 3339 timestamp of when the error occurred.
35    pub timestamp: String,
36}
37
38/// Check if debug endpoints should be enabled
39pub fn is_debug_enabled() -> bool {
40    // Disabled in production unless explicitly enabled
41    if Config::is_production() {
42        return std::env::var("FERRO_DEBUG_ENDPOINTS")
43            .map(|v| v == "true" || v == "1")
44            .unwrap_or(false);
45    }
46    true
47}
48
49/// Build a JSON response for debug endpoints
50fn json_response<T: Serialize>(data: T, status: u16) -> hyper::Response<Full<Bytes>> {
51    let body = serde_json::to_string_pretty(&data).unwrap_or_else(|_| "{}".to_string());
52    hyper::Response::builder()
53        .status(status)
54        .header("Content-Type", "application/json")
55        .body(Full::new(Bytes::from(body)))
56        .unwrap()
57}
58
59/// Handle /_ferro/routes endpoint
60pub fn handle_routes() -> hyper::Response<Full<Bytes>> {
61    if !is_debug_enabled() {
62        return json_response(
63            DebugErrorResponse {
64                success: false,
65                error: "Debug endpoints disabled in production".to_string(),
66                timestamp: Utc::now().to_rfc3339(),
67            },
68            403,
69        );
70    }
71
72    let routes = get_registered_routes();
73    json_response(
74        DebugResponse {
75            success: true,
76            data: routes,
77            timestamp: Utc::now().to_rfc3339(),
78        },
79        200,
80    )
81}
82
83/// Global middleware info for introspection
84#[derive(Debug, Serialize)]
85pub struct MiddlewareInfo {
86    /// Names of globally registered middleware, in registration order.
87    pub global: Vec<String>,
88}
89
90/// Handle /_ferro/middleware endpoint
91pub fn handle_middleware() -> hyper::Response<Full<Bytes>> {
92    if !is_debug_enabled() {
93        return json_response(
94            DebugErrorResponse {
95                success: false,
96                error: "Debug endpoints disabled in production".to_string(),
97                timestamp: Utc::now().to_rfc3339(),
98            },
99            403,
100        );
101    }
102
103    let global = get_global_middleware_info();
104    json_response(
105        DebugResponse {
106            success: true,
107            data: MiddlewareInfo { global },
108            timestamp: Utc::now().to_rfc3339(),
109        },
110        200,
111    )
112}
113
114/// Handle /_ferro/services endpoint
115pub fn handle_services() -> hyper::Response<Full<Bytes>> {
116    if !is_debug_enabled() {
117        return json_response(
118            DebugErrorResponse {
119                success: false,
120                error: "Debug endpoints disabled in production".to_string(),
121                timestamp: Utc::now().to_rfc3339(),
122            },
123            403,
124        );
125    }
126
127    let services = get_registered_services();
128    json_response(
129        DebugResponse {
130            success: true,
131            data: services,
132            timestamp: Utc::now().to_rfc3339(),
133        },
134        200,
135    )
136}
137
138/// Handle /_ferro/metrics endpoint
139pub fn handle_metrics() -> hyper::Response<Full<Bytes>> {
140    if !is_debug_enabled() {
141        return json_response(
142            DebugErrorResponse {
143                success: false,
144                error: "Debug endpoints disabled in production".to_string(),
145                timestamp: Utc::now().to_rfc3339(),
146            },
147            403,
148        );
149    }
150
151    let snapshot = metrics::get_metrics();
152    json_response(
153        DebugResponse {
154            success: true,
155            data: snapshot,
156            timestamp: Utc::now().to_rfc3339(),
157        },
158        200,
159    )
160}
161
162/// Queue jobs response
163#[derive(Debug, Serialize)]
164pub struct QueueJobsInfo {
165    /// Pending jobs (ready to process)
166    pub pending: Vec<ferro_queue::JobInfo>,
167    /// Delayed jobs (waiting for available_at)
168    pub delayed: Vec<ferro_queue::JobInfo>,
169    /// Failed jobs
170    pub failed: Vec<ferro_queue::FailedJobInfo>,
171}
172
173/// Handle /_ferro/queue/jobs endpoint
174pub async fn handle_queue_jobs() -> hyper::Response<Full<Bytes>> {
175    if !is_debug_enabled() {
176        return json_response(
177            DebugErrorResponse {
178                success: false,
179                error: "Debug endpoints disabled in production".to_string(),
180                timestamp: Utc::now().to_rfc3339(),
181            },
182            403,
183        );
184    }
185
186    // Check if queue is initialized
187    if !ferro_queue::Queue::is_initialized() {
188        return json_response(
189            DebugErrorResponse {
190                success: false,
191                error: "Queue not initialized (QUEUE_CONNECTION=sync or Redis not configured)"
192                    .to_string(),
193                timestamp: Utc::now().to_rfc3339(),
194            },
195            503,
196        );
197    }
198
199    let conn = ferro_queue::Queue::connection();
200    let default_queue = conn.config().default_queue.as_str();
201
202    // Fetch jobs from the default queue
203    let pending = conn
204        .get_pending_jobs(default_queue, 100)
205        .await
206        .unwrap_or_default();
207    let delayed = conn
208        .get_delayed_jobs(default_queue, 100)
209        .await
210        .unwrap_or_default();
211    let failed = conn.get_failed_jobs(100).await.unwrap_or_default();
212
213    json_response(
214        DebugResponse {
215            success: true,
216            data: QueueJobsInfo {
217                pending,
218                delayed,
219                failed,
220            },
221            timestamp: Utc::now().to_rfc3339(),
222        },
223        200,
224    )
225}
226
227/// Handle /_ferro/queue/stats endpoint
228pub async fn handle_queue_stats() -> hyper::Response<Full<Bytes>> {
229    if !is_debug_enabled() {
230        return json_response(
231            DebugErrorResponse {
232                success: false,
233                error: "Debug endpoints disabled in production".to_string(),
234                timestamp: Utc::now().to_rfc3339(),
235            },
236            403,
237        );
238    }
239
240    // Check if queue is initialized
241    if !ferro_queue::Queue::is_initialized() {
242        return json_response(
243            DebugErrorResponse {
244                success: false,
245                error: "Queue not initialized (QUEUE_CONNECTION=sync or Redis not configured)"
246                    .to_string(),
247                timestamp: Utc::now().to_rfc3339(),
248            },
249            503,
250        );
251    }
252
253    let conn = ferro_queue::Queue::connection();
254    let default_queue = conn.config().default_queue.as_str();
255
256    // Get stats for default queue
257    let stats = conn.get_stats(&[default_queue]).await.unwrap_or_default();
258
259    json_response(
260        DebugResponse {
261            success: true,
262            data: stats,
263            timestamp: Utc::now().to_rfc3339(),
264        },
265        200,
266    )
267}