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