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,
20 pub data: T,
21 pub timestamp: String,
22}
23
24#[derive(Debug, Serialize)]
26pub struct DebugErrorResponse {
27 pub success: bool,
28 pub error: String,
29 pub timestamp: String,
30}
31
32pub fn is_debug_enabled() -> bool {
34 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
43fn 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
53pub 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#[derive(Debug, Serialize)]
79pub struct MiddlewareInfo {
80 pub global: Vec<String>,
81}
82
83pub 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
107pub 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
131pub 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#[derive(Debug, Serialize)]
157pub struct QueueJobsInfo {
158 pub pending: Vec<ferro_queue::JobInfo>,
160 pub delayed: Vec<ferro_queue::JobInfo>,
162 pub failed: Vec<ferro_queue::FailedJobInfo>,
164}
165
166pub 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 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 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
220pub 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 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 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}