Skip to main content

meritocrab_api/
health.rs

1use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
2use serde::{Deserialize, Serialize};
3use std::time::Instant;
4
5use crate::state::AppState;
6
7/// Server start time (shared across all health checks)
8static mut SERVER_START_TIME: Option<Instant> = None;
9
10/// Initialize server start time
11pub fn init_server_start_time() {
12    unsafe {
13        SERVER_START_TIME = Some(Instant::now());
14    }
15}
16
17/// Get server uptime in seconds
18fn 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/// Health check response
27#[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/// Database connectivity status
37#[derive(Debug, Serialize, Deserialize)]
38pub struct DatabaseStatus {
39    pub connected: bool,
40    pub driver: String,
41}
42
43/// LLM provider status
44#[derive(Debug, Serialize, Deserialize)]
45pub struct LlmProviderStatus {
46    pub provider: String,
47    pub available: bool,
48}
49
50/// Health check endpoint
51///
52/// Returns 200 OK with comprehensive server info
53pub async fn health(State(state): State<AppState>) -> impl IntoResponse {
54    // Check database connectivity
55    let db_status = check_database_status(&state).await;
56
57    // Check LLM provider status
58    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
75/// Check database connectivity
76async fn check_database_status(state: &AppState) -> DatabaseStatus {
77    // Try a simple query to verify database is accessible
78    let connected = sqlx::query("SELECT 1")
79        .execute(&state.db_pool)
80        .await
81        .is_ok();
82
83    // Simplified driver detection - we use sqlx::Any which abstracts the driver
84    // In production this will typically be PostgreSQL, in dev it's SQLite
85    DatabaseStatus {
86        connected,
87        driver: "any".to_string(), // sqlx::Any abstracts the actual driver
88    }
89}
90
91/// Check LLM provider status
92fn check_llm_status(state: &AppState) -> LlmProviderStatus {
93    // For now, we assume if the evaluator exists, it's available
94    // In production, you might want to do a health check API call
95    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        // Initialize server start time
117        init_server_start_time();
118
119        // Install SQLite driver for sqlx::Any
120        sqlx::any::install_default_drivers();
121
122        // Create test database
123        let db_pool = AnyPoolOptions::new()
124            .max_connections(1)
125            .connect("sqlite::memory:")
126            .await
127            .expect("Failed to create test database");
128
129        // Create test GitHub client
130        let github_auth = GithubAppAuth::new(123456, "fake-private-key".to_string());
131        let mut token_manager = InstallationTokenManager::new(github_auth);
132        // Note: This will fail but we won't use GitHub in health check
133        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        // Create test state
137        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}