@@ -9,6 +9,7 @@ use axum::{
99 Json , Router ,
1010} ;
1111use chrono:: Utc ;
12+ use redis;
1213use serde:: Serialize ;
1314use std:: collections:: HashMap ;
1415use std:: net:: SocketAddr ;
@@ -32,6 +33,7 @@ use zaps_backend::services;
3233struct HealthState {
3334 pool : sqlx:: PgPool ,
3435 stellar_rpc_url : String ,
36+ redis_url : Option < String > ,
3537}
3638
3739#[ derive( Serialize ) ]
@@ -64,11 +66,20 @@ struct RpcHealth {
6466 error : Option < String > ,
6567}
6668
69+ #[ derive( Serialize ) ]
70+ struct RedisHealth {
71+ status : & ' static str ,
72+ latency_ms : u64 ,
73+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
74+ error : Option < String > ,
75+ }
76+
6777#[ derive( Serialize ) ]
6878struct HealthComponents {
6979 database : DbHealth ,
7080 yield_db : YieldDbHealth ,
7181 soroban_rpc : RpcHealth ,
82+ redis : RedisHealth ,
7283}
7384
7485#[ derive( Serialize ) ]
@@ -227,15 +238,18 @@ async fn main() {
227238 let bridge_state =
228239 api:: bridge:: BridgeState :: new ( pool. clone ( ) , config. allbridge_api_url . clone ( ) ) ;
229240
230- // Health check state: pool + Soroban RPC URL for live component probing.
241+ // Health check state: pool + Soroban RPC URL + Redis for live component probing.
231242 let health_state = HealthState {
232243 pool : pool. clone ( ) ,
233244 stellar_rpc_url : config. stellar_rpc_url . clone ( ) ,
245+ redis_url : config. redis_url . clone ( ) ,
234246 } ;
235247
236248 // Setup routes
237249 let public_routes = Router :: new ( )
238250 . route ( "/health" , get ( health_check) )
251+ . route ( "/healthz" , get ( liveness_probe) )
252+ . route ( "/readyz" , get ( readiness_probe) )
239253 . route ( "/api/v1/config" , get ( app_config) )
240254 . with_state ( health_state) ;
241255
@@ -390,21 +404,26 @@ async fn app_config() -> Json<AppConfigResponse> {
390404// ── /health handler ───────────────────────────────────────────────────────────
391405
392406async fn health_check ( State ( state) : State < HealthState > ) -> impl IntoResponse {
393- // Run all three probes concurrently so latencies don't stack.
394- let ( db, yield_db, rpc) = tokio:: join!(
407+ // Run all probes concurrently so latencies don't stack.
408+ let ( db, yield_db, rpc, redis ) = tokio:: join!(
395409 probe_database( & state. pool) ,
396410 probe_yield_db( & state. pool) ,
397411 probe_soroban_rpc( & state. stellar_rpc_url) ,
412+ probe_redis( state. redis_url. as_deref( ) ) ,
398413 ) ;
399414
400- let all_ok = db. status == "ok" && yield_db. status == "ok" && rpc. status == "ok" ;
415+ let all_ok = db. status == "ok"
416+ && yield_db. status == "ok"
417+ && rpc. status == "ok"
418+ && redis. status == "ok" ;
401419
402420 let body = HealthResponse {
403421 status : if all_ok { "ok" } else { "degraded" } ,
404422 components : HealthComponents {
405423 database : db,
406424 yield_db,
407425 soroban_rpc : rpc,
426+ redis,
408427 } ,
409428 checked_at : Utc :: now ( ) . to_rfc3339 ( ) ,
410429 } ;
@@ -418,6 +437,46 @@ async fn health_check(State(state): State<HealthState>) -> impl IntoResponse {
418437 ( code, Json ( body) )
419438}
420439
440+ /// GET /healthz — lightweight liveness probe for Kubernetes.
441+ /// Returns 200 if the process is alive; does not check dependencies.
442+ async fn liveness_probe ( ) -> impl IntoResponse {
443+ Json ( serde_json:: json!( { "status" : "ok" } ) )
444+ }
445+
446+ /// GET /readyz — Kubernetes readiness probe.
447+ /// Verifies DB and Redis are reachable; returns HTTP 200 only when both pass.
448+ async fn readiness_probe ( State ( state) : State < HealthState > ) -> impl IntoResponse {
449+ let ( db, redis) = tokio:: join!(
450+ probe_database( & state. pool) ,
451+ probe_redis( state. redis_url. as_deref( ) ) ,
452+ ) ;
453+
454+ let db_ok = db. status == "ok" ;
455+ let redis_ok = redis. status == "ok" || state. redis_url . is_none ( ) ;
456+
457+ let status = if db_ok && redis_ok { "ok" } else { "not ready" } ;
458+ let code = if db_ok && redis_ok {
459+ StatusCode :: OK
460+ } else {
461+ StatusCode :: SERVICE_UNAVAILABLE
462+ } ;
463+
464+ let mut body = serde_json:: json!( {
465+ "status" : status,
466+ "db" : db_ok,
467+ "redis" : redis_ok,
468+ } ) ;
469+
470+ if let Some ( e) = & db. error {
471+ body[ "db_error" ] = serde_json:: json!( e) ;
472+ }
473+ if let Some ( e) = & redis. error {
474+ body[ "redis_error" ] = serde_json:: json!( e) ;
475+ }
476+
477+ ( code, Json ( body) )
478+ }
479+
421480// ── Component probes ──────────────────────────────────────────────────────────
422481
423482/// Basic Postgres connectivity: a single round-trip to the DB pool.
@@ -537,3 +596,44 @@ async fn probe_soroban_rpc(rpc_url: &str) -> RpcHealth {
537596 } ,
538597 }
539598}
599+
600+ /// Redis PING probe. Returns "ok" when Redis responds with PONG.
601+ async fn probe_redis ( redis_url : Option < & str > ) -> RedisHealth {
602+ let start = Instant :: now ( ) ;
603+
604+ let Some ( url) = redis_url else {
605+ return RedisHealth {
606+ status : "skipped" ,
607+ latency_ms : 0 ,
608+ error : None ,
609+ } ;
610+ } ;
611+
612+ let result: Result < ( ) , String > = async {
613+ let client = redis:: Client :: open ( url) . map_err ( |e| e. to_string ( ) ) ?;
614+ let mut conn = redis:: tokio:: aio:: ConnectionManager :: new ( client)
615+ . await
616+ . map_err ( |e| e. to_string ( ) ) ?;
617+ redis:: cmd ( "PING" )
618+ . query_async :: < String > ( & mut conn)
619+ . await
620+ . map_err ( |e| e. to_string ( ) ) ?;
621+ Ok ( ( ) )
622+ }
623+ . await ;
624+
625+ let latency_ms = start. elapsed ( ) . as_millis ( ) as u64 ;
626+
627+ match result {
628+ Ok ( ( ) ) => RedisHealth {
629+ status : "ok" ,
630+ latency_ms,
631+ error : None ,
632+ } ,
633+ Err ( e) => RedisHealth {
634+ status : "error" ,
635+ latency_ms,
636+ error : Some ( e) ,
637+ } ,
638+ }
639+ }
0 commit comments