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