1use 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#[derive(Debug, Serialize)]
19pub struct DebugResponse<T: Serialize> {
20 pub success: bool,
22 pub data: T,
24 pub timestamp: String,
26}
27
28#[derive(Debug, Serialize)]
30pub struct DebugErrorResponse {
31 pub success: bool,
33 pub error: String,
35 pub timestamp: String,
37}
38
39pub fn is_debug_enabled() -> bool {
41 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
50fn 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
60pub 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#[derive(Debug, Serialize)]
86pub struct MiddlewareInfo {
87 pub global: Vec<String>,
89}
90
91pub 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
115pub 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
139pub 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#[derive(Debug, Serialize)]
165pub struct QueueJobsInfo {
166 pub pending: Vec<ferro_queue::JobInfo>,
168 pub delayed: Vec<ferro_queue::JobInfo>,
170 pub failed: Vec<ferro_queue::FailedJobInfo>,
172}
173
174pub 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 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 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
229pub 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 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 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}