Skip to content

Commit 6c44301

Browse files
authored
feat: Add endpoint for health checks (#89)
1 parent 2412ec5 commit 6c44301

File tree

8 files changed

+128
-1
lines changed

8 files changed

+128
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ clap = { version = "4.5.53", features = ["color", "derive", "help", "usage", "st
1818
redis = { version = "0.25.4", features = [ "ahash", "aio", "tokio-comp" ] }
1919
sha2 = "0.10.9"
2020

21+
[dev-dependencies]
22+
tower = "0.4"
23+
2124
[features]
2225
default = ["logging"]
2326
logging = ["dep:log", "dep:env_logger"]

src/db/mem.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{
33
convert::Infallible,
44
};
55

6-
use crate::Database;
6+
use super::Database;
77

88
#[derive(Clone)]
99
pub struct InMemoryDb {
@@ -74,4 +74,8 @@ impl Database for InMemoryDb {
7474
None => String::default(),
7575
}
7676
}
77+
78+
fn health_check(&self) -> Result<(), Self::Error> {
79+
Ok(())
80+
}
7781
}

src/db/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ pub trait Database: Clone {
2929
fn get_values(&self, namespace: &str) -> Result<HashSet<String>, Self::Error>;
3030
fn etag(&self, namespace: &str) -> String;
3131
fn delete_flag(&mut self, namespace: &str, flag: String) -> Result<bool, Self::Error>;
32+
fn health_check(&self) -> Result<(), Self::Error>;
3233
}

src/db/redis.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,10 @@ impl Database for RedisDb {
7474

7575
Ok(updated)
7676
}
77+
78+
fn health_check(&self) -> Result<(), Self::Error> {
79+
let mut connection = self.get_conn()?;
80+
let _: String = redis::cmd("PING").query(&mut connection)?;
81+
Ok(())
82+
}
7783
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod cfg;
2+
pub mod db;

src/main.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ async fn main() {
3131
api_key: cfg.api_key(),
3232
};
3333
let router = Router::new()
34+
.route("/health", get(health_check))
3435
.route("/api/flags/:namespace", get(get_ns).head(head_ns))
3536
.route("/api/flags/:namespace/:flag", put(put_flag).delete(delete_flag))
3637
.with_state(state);
@@ -106,6 +107,14 @@ async fn delete_flag(
106107
StatusCode::NO_CONTENT
107108
}
108109

110+
async fn health_check(state: State<AppState<impl Database>>) -> StatusCode {
111+
let db = state.0.db.read().unwrap();
112+
match db.health_check() {
113+
Ok(_) => StatusCode::OK,
114+
Err(_) => StatusCode::SERVICE_UNAVAILABLE,
115+
}
116+
}
117+
109118
#[derive(serde::Serialize)]
110119
struct Response {
111120
pub namespace: String,

tests/health_check.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use axum::Router;
2+
use axum::body::Body;
3+
use axum::http::{Request, StatusCode};
4+
use axum::routing::get;
5+
use std::sync::{Arc, RwLock};
6+
use tower::ServiceExt;
7+
8+
// Import the Database trait
9+
use flagpole::db::Database;
10+
11+
#[cfg(not(feature = "redis"))]
12+
use flagpole::db::mem::InMemoryDb;
13+
14+
#[cfg(feature = "redis")]
15+
use flagpole::db::redis::RedisDb;
16+
17+
#[derive(Clone)]
18+
struct AppState<T>
19+
where
20+
T: Database,
21+
{
22+
db: Arc<RwLock<T>>,
23+
api_key: Option<String>,
24+
}
25+
26+
async fn health_check_handler(
27+
axum::extract::State(state): axum::extract::State<AppState<impl Database>>,
28+
) -> StatusCode {
29+
let db = state.db.read().unwrap();
30+
match db.health_check() {
31+
Ok(_) => StatusCode::OK,
32+
Err(_) => StatusCode::SERVICE_UNAVAILABLE,
33+
}
34+
}
35+
36+
#[cfg(not(feature = "redis"))]
37+
#[tokio::test]
38+
async fn test_health_check_in_memory() {
39+
let state = AppState {
40+
db: Arc::new(RwLock::new(InMemoryDb::new())),
41+
api_key: None,
42+
};
43+
44+
let app = Router::new().route("/health", get(health_check_handler)).with_state(state);
45+
46+
let response = app
47+
.oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
48+
.await
49+
.unwrap();
50+
51+
assert_eq!(response.status(), StatusCode::OK);
52+
}
53+
54+
#[cfg(feature = "redis")]
55+
#[tokio::test]
56+
async fn test_health_check_redis_not_connected() {
57+
// Use an invalid Redis URI to simulate connection failure
58+
let state = AppState {
59+
db: Arc::new(RwLock::new(RedisDb::new("redis://invalid-host:6379".to_string()))),
60+
api_key: None,
61+
};
62+
63+
let app = Router::new().route("/health", get(health_check_handler)).with_state(state);
64+
65+
let response = app
66+
.oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
67+
.await
68+
.unwrap();
69+
70+
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
71+
}
72+
73+
#[cfg(feature = "redis")]
74+
#[tokio::test]
75+
async fn test_health_check_redis_connected() {
76+
// This test requires a running Redis instance at localhost:6379
77+
// Skip if Redis is not available
78+
let redis_uri =
79+
std::env::var("REDIS_URI").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
80+
81+
let state = AppState {
82+
db: Arc::new(RwLock::new(RedisDb::new(redis_uri))),
83+
api_key: None,
84+
};
85+
86+
let app = Router::new().route("/health", get(health_check_handler)).with_state(state);
87+
88+
let response = app
89+
.oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
90+
.await
91+
.unwrap();
92+
93+
// This will pass if Redis is available, otherwise it will fail
94+
// In a real CI/CD setup, you'd ensure Redis is running or skip this test
95+
let status = response.status();
96+
assert!(
97+
status == StatusCode::OK || status == StatusCode::SERVICE_UNAVAILABLE,
98+
"Expected OK or SERVICE_UNAVAILABLE, got {:?}",
99+
status
100+
);
101+
}

0 commit comments

Comments
 (0)