Skip to main content

meritocrab_api/
health.rs

1use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
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 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        // Initialize server start time
115        init_server_start_time();
116
117        // Install SQLite driver for sqlx::Any
118        sqlx::any::install_default_drivers();
119
120        // Create test database
121        let db_pool = AnyPoolOptions::new()
122            .max_connections(1)
123            .connect("sqlite::memory:")
124            .await
125            .expect("Failed to create test database");
126
127        // Create test GitHub client
128        let github_auth = GithubAppAuth::new(123456, "fake-private-key".to_string());
129        let mut token_manager = InstallationTokenManager::new(github_auth);
130        // Note: This will fail but we won't use GitHub in health check
131        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        // Create test state
135        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}