1use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
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 crate::OAuthConfig;
105 use axum::extract::State;
106 use meritocrab_core::RepoConfig;
107 use meritocrab_github::{
108 GithubApiClient, GithubAppAuth, InstallationTokenManager, WebhookSecret,
109 };
110 use meritocrab_llm::MockEvaluator;
111 use sqlx::any::AnyPoolOptions;
112 use std::sync::Arc;
113
114 #[tokio::test]
115 async fn test_health_endpoint() {
116 init_server_start_time();
118
119 sqlx::any::install_default_drivers();
121
122 let db_pool = AnyPoolOptions::new()
124 .max_connections(1)
125 .connect("sqlite::memory:")
126 .await
127 .expect("Failed to create test database");
128
129 let github_auth = GithubAppAuth::new(123456, "fake-private-key".to_string());
131 let mut token_manager = InstallationTokenManager::new(github_auth);
132 let token = token_manager.get_token(123456).await.unwrap_or_default();
134 let github_client = GithubApiClient::new(token).expect("Failed to create GitHub client");
135
136 let app_state = AppState::new(
138 db_pool,
139 github_client,
140 RepoConfig::default(),
141 WebhookSecret::new("test-secret".to_string()),
142 Arc::new(MockEvaluator::new()),
143 5,
144 OAuthConfig {
145 client_id: "test".to_string(),
146 client_secret: "test".to_string(),
147 redirect_url: "http://localhost/callback".to_string(),
148 },
149 300,
150 );
151
152 let response = health(State(app_state)).await.into_response();
153 assert_eq!(response.status(), StatusCode::OK);
154 }
155}