1use crate::analyzer::{DetectedTechnology, HealthEndpoint, HealthEndpointSource, TechnologyCategory};
9use crate::common::file_utils::{is_readable_file, read_file_safe};
10use crate::error::Result;
11use regex::Regex;
12use std::path::Path;
13
14const COMMON_HEALTH_PATHS: &[&str] = &[
16 "/health",
17 "/healthz",
18 "/ready",
19 "/readyz",
20 "/livez",
21 "/live",
22 "/api/health",
23 "/api/v1/health",
24 "/__health",
25 "/ping",
26 "/status",
27];
28
29pub fn detect_health_endpoints(
31 project_root: &Path,
32 technologies: &[DetectedTechnology],
33 max_file_size: usize,
34) -> Vec<HealthEndpoint> {
35 let mut endpoints = Vec::new();
36
37 for tech in technologies {
39 if let Some(endpoint) = get_framework_health_endpoint(tech) {
40 endpoints.push(endpoint);
41 }
42 }
43
44 let detected_from_code = scan_for_health_routes(project_root, technologies, max_file_size);
46 for endpoint in detected_from_code {
47 if !endpoints.iter().any(|e| e.path == endpoint.path) {
49 endpoints.push(endpoint);
50 } else {
51 if let Some(existing) = endpoints.iter_mut().find(|e| e.path == endpoint.path) {
53 if endpoint.confidence > existing.confidence {
54 *existing = endpoint;
55 }
56 }
57 }
58 }
59
60 endpoints.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
62
63 endpoints
64}
65
66fn get_framework_health_endpoint(tech: &DetectedTechnology) -> Option<HealthEndpoint> {
68 match tech.name.as_str() {
69 "Spring Boot" => Some(HealthEndpoint::from_framework("/actuator/health", "Spring Boot Actuator")),
71 "Quarkus" => Some(HealthEndpoint::from_framework("/q/health", "Quarkus SmallRye Health")),
72 "Micronaut" => Some(HealthEndpoint::from_framework("/health", "Micronaut")),
73
74 "Express" | "Fastify" | "Koa" | "Hono" | "Elysia" | "NestJS" => {
76 Some(HealthEndpoint {
78 path: "/health".to_string(),
79 confidence: 0.5,
80 source: HealthEndpointSource::FrameworkDefault,
81 description: Some(format!("{} common health pattern", tech.name)),
82 })
83 }
84
85 "FastAPI" => Some(HealthEndpoint::from_framework("/health", "FastAPI")),
87 "Django" => Some(HealthEndpoint {
88 path: "/health/".to_string(), confidence: 0.5,
90 source: HealthEndpointSource::FrameworkDefault,
91 description: Some("Django common health pattern".to_string()),
92 }),
93 "Flask" => Some(HealthEndpoint {
94 path: "/health".to_string(),
95 confidence: 0.5,
96 source: HealthEndpointSource::FrameworkDefault,
97 description: Some("Flask common health pattern".to_string()),
98 }),
99
100 "Gin" | "Echo" | "Fiber" | "Chi" => Some(HealthEndpoint {
102 path: "/health".to_string(),
103 confidence: 0.5,
104 source: HealthEndpointSource::FrameworkDefault,
105 description: Some(format!("{} common health pattern", tech.name)),
106 }),
107
108 "Actix Web" | "Axum" | "Rocket" => Some(HealthEndpoint {
110 path: "/health".to_string(),
111 confidence: 0.5,
112 source: HealthEndpointSource::FrameworkDefault,
113 description: Some(format!("{} common health pattern", tech.name)),
114 }),
115
116 _ => None,
117 }
118}
119
120fn scan_for_health_routes(
122 project_root: &Path,
123 technologies: &[DetectedTechnology],
124 max_file_size: usize,
125) -> Vec<HealthEndpoint> {
126 let mut endpoints = Vec::new();
127
128 let has_js = technologies.iter().any(|t| {
130 matches!(t.category, TechnologyCategory::BackendFramework | TechnologyCategory::MetaFramework)
131 && (t.name.contains("Express") || t.name.contains("Fastify") || t.name.contains("Koa")
132 || t.name.contains("Hono") || t.name.contains("Elysia") || t.name.contains("NestJS")
133 || t.name.contains("Next") || t.name.contains("Nuxt"))
134 });
135
136 let has_python = technologies.iter().any(|t| {
137 matches!(t.category, TechnologyCategory::BackendFramework)
138 && (t.name.contains("FastAPI") || t.name.contains("Flask") || t.name.contains("Django"))
139 });
140
141 let has_go = technologies.iter().any(|t| {
142 matches!(t.category, TechnologyCategory::BackendFramework)
143 && (t.name.contains("Gin") || t.name.contains("Echo") || t.name.contains("Fiber") || t.name.contains("Chi"))
144 });
145
146 let has_rust = technologies.iter().any(|t| {
147 matches!(t.category, TechnologyCategory::BackendFramework)
148 && (t.name.contains("Actix") || t.name.contains("Axum") || t.name.contains("Rocket"))
149 });
150
151 let has_java = technologies.iter().any(|t| {
152 matches!(t.category, TechnologyCategory::BackendFramework)
153 && (t.name.contains("Spring") || t.name.contains("Quarkus") || t.name.contains("Micronaut"))
154 });
155
156 let locations = [
158 "src/",
159 "app/",
160 "routes/",
161 "api/",
162 "server/",
163 "lib/",
164 "handlers/",
165 "controllers/",
166 ];
167
168 for location in &locations {
169 let dir = project_root.join(location);
170 if dir.is_dir() {
171 if has_js {
172 scan_directory_for_patterns(&dir, &["js", "ts", "mjs"], &js_health_patterns(), max_file_size, &mut endpoints);
173 }
174 if has_python {
175 scan_directory_for_patterns(&dir, &["py"], &python_health_patterns(), max_file_size, &mut endpoints);
176 }
177 if has_go {
178 scan_directory_for_patterns(&dir, &["go"], &go_health_patterns(), max_file_size, &mut endpoints);
179 }
180 if has_rust {
181 scan_directory_for_patterns(&dir, &["rs"], &rust_health_patterns(), max_file_size, &mut endpoints);
182 }
183 if has_java {
184 scan_directory_for_patterns(&dir, &["java", "kt"], &java_health_patterns(), max_file_size, &mut endpoints);
185 }
186 }
187 }
188
189 if has_js {
191 for entry in ["index.js", "index.ts", "app.js", "app.ts", "server.js", "server.ts", "main.js", "main.ts"] {
192 let path = project_root.join(entry);
193 if is_readable_file(&path) {
194 scan_file_for_patterns(&path, &js_health_patterns(), max_file_size, &mut endpoints);
195 }
196 }
197 }
198 if has_python {
199 for entry in ["main.py", "app.py", "wsgi.py", "asgi.py"] {
200 let path = project_root.join(entry);
201 if is_readable_file(&path) {
202 scan_file_for_patterns(&path, &python_health_patterns(), max_file_size, &mut endpoints);
203 }
204 }
205 }
206 if has_go {
207 let main_go = project_root.join("main.go");
208 if is_readable_file(&main_go) {
209 scan_file_for_patterns(&main_go, &go_health_patterns(), max_file_size, &mut endpoints);
210 }
211 }
212 if has_rust {
213 let main_rs = project_root.join("src/main.rs");
214 if is_readable_file(&main_rs) {
215 scan_file_for_patterns(&main_rs, &rust_health_patterns(), max_file_size, &mut endpoints);
216 }
217 }
218
219 endpoints
220}
221
222fn scan_directory_for_patterns(
224 dir: &Path,
225 extensions: &[&str],
226 patterns: &[(&str, f32)],
227 max_file_size: usize,
228 endpoints: &mut Vec<HealthEndpoint>,
229) {
230 if let Ok(entries) = std::fs::read_dir(dir) {
231 for entry in entries.flatten() {
232 let path = entry.path();
233 if path.is_file() {
234 if let Some(ext) = path.extension() {
235 if extensions.iter().any(|e| ext == *e) {
236 scan_file_for_patterns(&path, patterns, max_file_size, endpoints);
237 }
238 }
239 } else if path.is_dir() {
240 let dir_name = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
242 if !["node_modules", ".git", "target", "build", "dist", "__pycache__", ".next", "vendor"].contains(&dir_name.as_str()) {
243 scan_directory_for_patterns(&path, extensions, patterns, max_file_size, endpoints);
244 }
245 }
246 }
247 }
248}
249
250fn scan_file_for_patterns(
252 path: &Path,
253 patterns: &[(&str, f32)],
254 max_file_size: usize,
255 endpoints: &mut Vec<HealthEndpoint>,
256) {
257 if let Ok(content) = read_file_safe(path, max_file_size) {
258 for (pattern, confidence) in patterns {
259 if let Ok(regex) = Regex::new(pattern) {
260 for cap in regex.captures_iter(&content) {
261 if let Some(path_match) = cap.get(1) {
262 let health_path = path_match.as_str().to_string();
263 if COMMON_HEALTH_PATHS.iter().any(|p| health_path.contains(p) || p.contains(&health_path)) {
265 if !endpoints.iter().any(|e| e.path == health_path) {
266 endpoints.push(HealthEndpoint {
267 path: health_path,
268 confidence: *confidence,
269 source: HealthEndpointSource::CodePattern,
270 description: Some(format!("Found in {}", path.display())),
271 });
272 }
273 }
274 }
275 }
276 }
277 }
278 }
279}
280
281fn js_health_patterns() -> Vec<(&'static str, f32)> {
283 vec[^'"]*)['"]"#, 0.9),
286 (r#"@Get\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#, 0.9),
288 (r#"\.get\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#, 0.9),
290 ]
291}
292
293fn python_health_patterns() -> Vec<(&'static str, f32)> {
295 vec[^'"]*)['"]"#, 0.9),
298 (r#"path\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#, 0.85),
300 ]
301}
302
303fn go_health_patterns() -> Vec<(&'static str, f32)> {
305 vec![
306 (r#"HandleFunc\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#, 0.9),
308 (r#"\.(?:GET|Handle)\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#, 0.9),
310 ]
311}
312
313fn rust_health_patterns() -> Vec<(&'static str, f32)> {
315 vec![
316 (r#"\.route\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#, 0.9),
318 (r#"\.route\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#, 0.9),
320 ]
321}
322
323fn java_health_patterns() -> Vec<(&'static str, f32)> {
325 vec[^"']*)["']"#, 0.9),
328 ]
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn test_spring_boot_health_endpoint() {
337 let tech = DetectedTechnology {
338 name: "Spring Boot".to_string(),
339 version: None,
340 category: TechnologyCategory::BackendFramework,
341 confidence: 0.9,
342 requires: vec![],
343 conflicts_with: vec![],
344 is_primary: true,
345 file_indicators: vec![],
346 };
347
348 let endpoint = get_framework_health_endpoint(&tech).unwrap();
349 assert_eq!(endpoint.path, "/actuator/health");
350 assert_eq!(endpoint.confidence, 0.7);
351 }
352
353 #[test]
354 fn test_express_health_endpoint() {
355 let tech = DetectedTechnology {
356 name: "Express".to_string(),
357 version: None,
358 category: TechnologyCategory::BackendFramework,
359 confidence: 0.9,
360 requires: vec![],
361 conflicts_with: vec![],
362 is_primary: true,
363 file_indicators: vec![],
364 };
365
366 let endpoint = get_framework_health_endpoint(&tech).unwrap();
367 assert_eq!(endpoint.path, "/health");
368 assert_eq!(endpoint.confidence, 0.5); }
370
371 #[test]
372 fn test_unknown_framework_no_endpoint() {
373 let tech = DetectedTechnology {
374 name: "UnknownFramework".to_string(),
375 version: None,
376 category: TechnologyCategory::BackendFramework,
377 confidence: 0.9,
378 requires: vec![],
379 conflicts_with: vec![],
380 is_primary: true,
381 file_indicators: vec![],
382 };
383
384 assert!(get_framework_health_endpoint(&tech).is_none());
385 }
386}