1use 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#[derive(Debug, Serialize)]
18pub struct DebugResponse<T: Serialize> {
19 pub success: bool,
21 pub data: T,
23 pub timestamp: String,
25}
26
27#[derive(Debug, Serialize)]
29pub struct DebugErrorResponse {
30 pub success: bool,
32 pub error: String,
34 pub timestamp: String,
36}
37
38pub fn is_debug_enabled() -> bool {
40 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
49fn 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
59pub 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#[derive(Debug, Serialize)]
85pub struct MiddlewareInfo {
86 pub global: Vec<String>,
88}
89
90pub 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
114pub 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
138pub 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#[derive(Debug, Serialize)]
164pub struct QueueJobsInfo {
165 pub pending: Vec<ferro_queue::JobInfo>,
167 pub delayed: Vec<ferro_queue::JobInfo>,
169 pub failed: Vec<ferro_queue::FailedJobInfo>,
171}
172
173pub 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 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 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
227pub 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 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 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}