1use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
2use serde::{Deserialize, Serialize};
3use std::time::Instant;
4
5use crate::state::AppState;
6
7static mut SERVER_START_TIME: Option<Instant> = None;
9
10pub fn init_server_start_time() {
12 unsafe {
13 SERVER_START_TIME = Some(Instant::now());
14 }
15}
16
17fn get_uptime_seconds() -> u64 {
19 unsafe {
20 SERVER_START_TIME
21 .map(|start| start.elapsed().as_secs())
22 .unwrap_or(0)
23 }
24}
25
26#[derive(Debug, Serialize, Deserialize)]
28pub struct HealthResponse {
29 pub status: String,
30 pub version: String,
31 pub uptime_seconds: u64,
32 pub database: DatabaseStatus,
33 pub llm_provider: LlmProviderStatus,
34}
35
36#[derive(Debug, Serialize, Deserialize)]
38pub struct DatabaseStatus {
39 pub connected: bool,
40 pub driver: String,
41}
42
43#[derive(Debug, Serialize, Deserialize)]
45pub struct LlmProviderStatus {
46 pub provider: String,
47 pub available: bool,
48}
49
50pub async fn health(State(state): State<AppState>) -> impl IntoResponse {
54 let db_status = check_database_status(&state).await;
56
57 let llm_status = check_llm_status(&state);
59
60 let response = HealthResponse {
61 status: if db_status.connected && llm_status.available {
62 "healthy".to_string()
63 } else {
64 "degraded".to_string()
65 },
66 version: env!("CARGO_PKG_VERSION").to_string(),
67 uptime_seconds: get_uptime_seconds(),
68 database: db_status,
69 llm_provider: llm_status,
70 };
71
72 (StatusCode::OK, Json(response))
73}
74
75async fn check_database_status(state: &AppState) -> DatabaseStatus {
77 let connected = sqlx::query("SELECT 1")
79 .execute(&state.db_pool)
80 .await
81 .is_ok();
82
83 DatabaseStatus {
86 connected,
87 driver: "any".to_string(), }
89}
90
91fn check_llm_status(state: &AppState) -> LlmProviderStatus {
93 LlmProviderStatus {
96 provider: state.llm_evaluator.provider_name(),
97 available: true,
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use axum::extract::State;
105 use meritocrab_core::RepoConfig;
106 use meritocrab_github::{GithubApiClient, GithubAppAuth, InstallationTokenManager, WebhookSecret};
107 use meritocrab_llm::MockEvaluator;
108 use sqlx::any::AnyPoolOptions;
109 use std::sync::Arc;
110 use crate::OAuthConfig;
111
112 #[tokio::test]
113 async fn test_health_endpoint() {
114 init_server_start_time();
116
117 sqlx::any::install_default_drivers();
119
120 let db_pool = AnyPoolOptions::new()
122 .max_connections(1)
123 .connect("sqlite::memory:")
124 .await
125 .expect("Failed to create test database");
126
127 let github_auth = GithubAppAuth::new(123456, "fake-private-key".to_string());
129 let mut token_manager = InstallationTokenManager::new(github_auth);
130 let token = token_manager.get_token(123456).await.unwrap_or_default();
132 let github_client = GithubApiClient::new(token).expect("Failed to create GitHub client");
133
134 let app_state = AppState::new(
136 db_pool,
137 github_client,
138 RepoConfig::default(),
139 WebhookSecret::new("test-secret".to_string()),
140 Arc::new(MockEvaluator::new()),
141 5,
142 OAuthConfig {
143 client_id: "test".to_string(),
144 client_secret: "test".to_string(),
145 redirect_url: "http://localhost/callback".to_string(),
146 },
147 300,
148 );
149
150 let response = health(State(app_state)).await.into_response();
151 assert_eq!(response.status(), StatusCode::OK);
152 }
153}