mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
feat: add deep health check (#3210)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
6
crates/api_models/src/health_check.rs
Normal file
6
crates/api_models/src/health_check.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct RouterHealthCheckResponse {
|
||||||
|
pub database: String,
|
||||||
|
pub redis: String,
|
||||||
|
pub locker: String,
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@ pub mod errors;
|
|||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod files;
|
pub mod files;
|
||||||
pub mod gsm;
|
pub mod gsm;
|
||||||
|
pub mod health_check;
|
||||||
pub mod locker_migration;
|
pub mod locker_migration;
|
||||||
pub mod mandates;
|
pub mod mandates;
|
||||||
pub mod organization;
|
pub mod organization;
|
||||||
|
|||||||
@ -70,3 +70,5 @@ pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day
|
|||||||
pub const VERIFY_CONNECTOR_ID_PREFIX: &str = "conn_verify";
|
pub const VERIFY_CONNECTOR_ID_PREFIX: &str = "conn_verify";
|
||||||
#[cfg(feature = "olap")]
|
#[cfg(feature = "olap")]
|
||||||
pub const VERIFY_CONNECTOR_MERCHANT_ID: &str = "test_merchant";
|
pub const VERIFY_CONNECTOR_MERCHANT_ID: &str = "test_merchant";
|
||||||
|
|
||||||
|
pub const LOCKER_HEALTH_CALL_PATH: &str = "/health";
|
||||||
|
|||||||
@ -14,6 +14,7 @@ pub mod events;
|
|||||||
pub mod file;
|
pub mod file;
|
||||||
pub mod fraud_check;
|
pub mod fraud_check;
|
||||||
pub mod gsm;
|
pub mod gsm;
|
||||||
|
pub mod health_check;
|
||||||
mod kafka_store;
|
mod kafka_store;
|
||||||
pub mod locker_mock_up;
|
pub mod locker_mock_up;
|
||||||
pub mod mandate;
|
pub mod mandate;
|
||||||
@ -103,6 +104,7 @@ pub trait StorageInterface:
|
|||||||
+ user_role::UserRoleInterface
|
+ user_role::UserRoleInterface
|
||||||
+ authorization::AuthorizationInterface
|
+ authorization::AuthorizationInterface
|
||||||
+ user::sample_data::BatchSampleDataInterface
|
+ user::sample_data::BatchSampleDataInterface
|
||||||
|
+ health_check::HealthCheckInterface
|
||||||
+ 'static
|
+ 'static
|
||||||
{
|
{
|
||||||
fn get_scheduler_db(&self) -> Box<dyn scheduler::SchedulerInterface>;
|
fn get_scheduler_db(&self) -> Box<dyn scheduler::SchedulerInterface>;
|
||||||
|
|||||||
147
crates/router/src/db/health_check.rs
Normal file
147
crates/router/src/db/health_check.rs
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
use async_bb8_diesel::{AsyncConnection, AsyncRunQueryDsl};
|
||||||
|
use diesel_models::ConfigNew;
|
||||||
|
use error_stack::ResultExt;
|
||||||
|
use router_env::logger;
|
||||||
|
|
||||||
|
use super::{MockDb, StorageInterface, Store};
|
||||||
|
use crate::{
|
||||||
|
connection,
|
||||||
|
consts::LOCKER_HEALTH_CALL_PATH,
|
||||||
|
core::errors::{self, CustomResult},
|
||||||
|
routes,
|
||||||
|
services::api as services,
|
||||||
|
types::storage,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait HealthCheckInterface {
|
||||||
|
async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError>;
|
||||||
|
async fn health_check_redis(
|
||||||
|
&self,
|
||||||
|
db: &dyn StorageInterface,
|
||||||
|
) -> CustomResult<(), errors::HealthCheckRedisError>;
|
||||||
|
async fn health_check_locker(
|
||||||
|
&self,
|
||||||
|
state: &routes::AppState,
|
||||||
|
) -> CustomResult<(), errors::HealthCheckLockerError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl HealthCheckInterface for Store {
|
||||||
|
async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError> {
|
||||||
|
let conn = connection::pg_connection_write(self)
|
||||||
|
.await
|
||||||
|
.change_context(errors::HealthCheckDBError::DBError)?;
|
||||||
|
|
||||||
|
let _data = conn
|
||||||
|
.transaction_async(|conn| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let query =
|
||||||
|
diesel::select(diesel::dsl::sql::<diesel::sql_types::Integer>("1 + 1"));
|
||||||
|
let _x: i32 = query.get_result_async(&conn).await.map_err(|err| {
|
||||||
|
logger::error!(read_err=?err,"Error while reading element in the database");
|
||||||
|
errors::HealthCheckDBError::DBReadError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
logger::debug!("Database read was successful");
|
||||||
|
|
||||||
|
let config = ConfigNew {
|
||||||
|
key: "test_key".to_string(),
|
||||||
|
config: "test_value".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
config.insert(&conn).await.map_err(|err| {
|
||||||
|
logger::error!(write_err=?err,"Error while writing to database");
|
||||||
|
errors::HealthCheckDBError::DBWriteError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
logger::debug!("Database write was successful");
|
||||||
|
|
||||||
|
storage::Config::delete_by_key(&conn, "test_key").await.map_err(|err| {
|
||||||
|
logger::error!(delete_err=?err,"Error while deleting element in the database");
|
||||||
|
errors::HealthCheckDBError::DBDeleteError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
logger::debug!("Database delete was successful");
|
||||||
|
|
||||||
|
Ok::<_, errors::HealthCheckDBError>(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_check_redis(
|
||||||
|
&self,
|
||||||
|
db: &dyn StorageInterface,
|
||||||
|
) -> CustomResult<(), errors::HealthCheckRedisError> {
|
||||||
|
let redis_conn = db
|
||||||
|
.get_redis_conn()
|
||||||
|
.change_context(errors::HealthCheckRedisError::RedisConnectionError)?;
|
||||||
|
|
||||||
|
redis_conn
|
||||||
|
.serialize_and_set_key_with_expiry("test_key", "test_value", 30)
|
||||||
|
.await
|
||||||
|
.change_context(errors::HealthCheckRedisError::SetFailed)?;
|
||||||
|
|
||||||
|
logger::debug!("Redis set_key was successful");
|
||||||
|
|
||||||
|
redis_conn
|
||||||
|
.get_key("test_key")
|
||||||
|
.await
|
||||||
|
.change_context(errors::HealthCheckRedisError::GetFailed)?;
|
||||||
|
|
||||||
|
logger::debug!("Redis get_key was successful");
|
||||||
|
|
||||||
|
redis_conn
|
||||||
|
.delete_key("test_key")
|
||||||
|
.await
|
||||||
|
.change_context(errors::HealthCheckRedisError::DeleteFailed)?;
|
||||||
|
|
||||||
|
logger::debug!("Redis delete_key was successful");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_check_locker(
|
||||||
|
&self,
|
||||||
|
state: &routes::AppState,
|
||||||
|
) -> CustomResult<(), errors::HealthCheckLockerError> {
|
||||||
|
let locker = &state.conf.locker;
|
||||||
|
if !locker.mock_locker {
|
||||||
|
let mut url = locker.host_rs.to_owned();
|
||||||
|
url.push_str(LOCKER_HEALTH_CALL_PATH);
|
||||||
|
let request = services::Request::new(services::Method::Get, &url);
|
||||||
|
services::call_connector_api(state, request)
|
||||||
|
.await
|
||||||
|
.change_context(errors::HealthCheckLockerError::FailedToCallLocker)?
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger::debug!("Locker call was successful");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl HealthCheckInterface for MockDb {
|
||||||
|
async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_check_redis(
|
||||||
|
&self,
|
||||||
|
_: &dyn StorageInterface,
|
||||||
|
) -> CustomResult<(), errors::HealthCheckRedisError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_check_locker(
|
||||||
|
&self,
|
||||||
|
_: &routes::AppState,
|
||||||
|
) -> CustomResult<(), errors::HealthCheckLockerError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -43,6 +43,7 @@ use crate::{
|
|||||||
events::EventInterface,
|
events::EventInterface,
|
||||||
file::FileMetadataInterface,
|
file::FileMetadataInterface,
|
||||||
gsm::GsmInterface,
|
gsm::GsmInterface,
|
||||||
|
health_check::HealthCheckInterface,
|
||||||
locker_mock_up::LockerMockUpInterface,
|
locker_mock_up::LockerMockUpInterface,
|
||||||
mandate::MandateInterface,
|
mandate::MandateInterface,
|
||||||
merchant_account::MerchantAccountInterface,
|
merchant_account::MerchantAccountInterface,
|
||||||
@ -57,6 +58,7 @@ use crate::{
|
|||||||
routing_algorithm::RoutingAlgorithmInterface,
|
routing_algorithm::RoutingAlgorithmInterface,
|
||||||
MasterKeyInterface, StorageInterface,
|
MasterKeyInterface, StorageInterface,
|
||||||
},
|
},
|
||||||
|
routes,
|
||||||
services::{authentication, kafka::KafkaProducer, Store},
|
services::{authentication, kafka::KafkaProducer, Store},
|
||||||
types::{
|
types::{
|
||||||
domain,
|
domain,
|
||||||
@ -2131,3 +2133,24 @@ impl AuthorizationInterface for KafkaStore {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl HealthCheckInterface for KafkaStore {
|
||||||
|
async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError> {
|
||||||
|
self.diesel_store.health_check_db().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_check_redis(
|
||||||
|
&self,
|
||||||
|
db: &dyn StorageInterface,
|
||||||
|
) -> CustomResult<(), errors::HealthCheckRedisError> {
|
||||||
|
self.diesel_store.health_check_redis(db).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_check_locker(
|
||||||
|
&self,
|
||||||
|
state: &routes::AppState,
|
||||||
|
) -> CustomResult<(), errors::HealthCheckLockerError> {
|
||||||
|
self.diesel_store.health_check_locker(state).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -253,9 +253,10 @@ pub struct Health;
|
|||||||
|
|
||||||
impl Health {
|
impl Health {
|
||||||
pub fn server(state: AppState) -> Scope {
|
pub fn server(state: AppState) -> Scope {
|
||||||
web::scope("")
|
web::scope("health")
|
||||||
.app_data(web::Data::new(state))
|
.app_data(web::Data::new(state))
|
||||||
.service(web::resource("/health").route(web::get().to(health)))
|
.service(web::resource("").route(web::get().to(health)))
|
||||||
|
.service(web::resource("/deep_check").route(web::post().to(deep_health_check)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
|
use actix_web::web;
|
||||||
|
use api_models::health_check::RouterHealthCheckResponse;
|
||||||
use router_env::{instrument, logger, tracing};
|
use router_env::{instrument, logger, tracing};
|
||||||
|
|
||||||
use crate::routes::metrics;
|
use super::app;
|
||||||
|
use crate::{routes::metrics, services};
|
||||||
/// .
|
/// .
|
||||||
// #[logger::instrument(skip_all, name = "name1", level = "warn", fields( key1 = "val1" ))]
|
// #[logger::instrument(skip_all, name = "name1", level = "warn", fields( key1 = "val1" ))]
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
@ -11,3 +13,59 @@ pub async fn health() -> impl actix_web::Responder {
|
|||||||
logger::info!("Health was called");
|
logger::info!("Health was called");
|
||||||
actix_web::HttpResponse::Ok().body("health is good")
|
actix_web::HttpResponse::Ok().body("health is good")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn deep_health_check(state: web::Data<app::AppState>) -> impl actix_web::Responder {
|
||||||
|
metrics::HEALTH_METRIC.add(&metrics::CONTEXT, 1, &[]);
|
||||||
|
let db = &*state.store;
|
||||||
|
let mut status_code = 200;
|
||||||
|
logger::info!("Deep health check was called");
|
||||||
|
|
||||||
|
logger::debug!("Database health check begin");
|
||||||
|
|
||||||
|
let db_status = match db.health_check_db().await {
|
||||||
|
Ok(_) => "Health is good".to_string(),
|
||||||
|
Err(err) => {
|
||||||
|
status_code = 500;
|
||||||
|
err.to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
logger::debug!("Database health check end");
|
||||||
|
|
||||||
|
logger::debug!("Redis health check begin");
|
||||||
|
|
||||||
|
let redis_status = match db.health_check_redis(db).await {
|
||||||
|
Ok(_) => "Health is good".to_string(),
|
||||||
|
Err(err) => {
|
||||||
|
status_code = 500;
|
||||||
|
err.to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
logger::debug!("Redis health check end");
|
||||||
|
|
||||||
|
logger::debug!("Locker health check begin");
|
||||||
|
|
||||||
|
let locker_status = match db.health_check_locker(&state).await {
|
||||||
|
Ok(_) => "Health is good".to_string(),
|
||||||
|
Err(err) => {
|
||||||
|
status_code = 500;
|
||||||
|
err.to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
logger::debug!("Locker health check end");
|
||||||
|
|
||||||
|
let response = serde_json::to_string(&RouterHealthCheckResponse {
|
||||||
|
database: db_status,
|
||||||
|
redis: redis_status,
|
||||||
|
locker: locker_status,
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if status_code == 200 {
|
||||||
|
services::http_response_json(response)
|
||||||
|
} else {
|
||||||
|
services::http_server_error_json_response(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1138,6 +1138,14 @@ pub fn http_response_json<T: body::MessageBody + 'static>(response: T) -> HttpRe
|
|||||||
.body(response)
|
.body(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn http_server_error_json_response<T: body::MessageBody + 'static>(
|
||||||
|
response: T,
|
||||||
|
) -> HttpResponse {
|
||||||
|
HttpResponse::InternalServerError()
|
||||||
|
.content_type(mime::APPLICATION_JSON)
|
||||||
|
.body(response)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn http_response_json_with_headers<T: body::MessageBody + 'static>(
|
pub fn http_response_json_with_headers<T: body::MessageBody + 'static>(
|
||||||
response: T,
|
response: T,
|
||||||
mut headers: Vec<(String, String)>,
|
mut headers: Vec<(String, String)>,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ use router_env::tracing_actix_web::RequestId;
|
|||||||
use super::{request::Maskable, Request};
|
use super::{request::Maskable, Request};
|
||||||
use crate::{
|
use crate::{
|
||||||
configs::settings::{Locker, Proxy},
|
configs::settings::{Locker, Proxy},
|
||||||
|
consts::LOCKER_HEALTH_CALL_PATH,
|
||||||
core::{
|
core::{
|
||||||
errors::{ApiClientError, CustomResult},
|
errors::{ApiClientError, CustomResult},
|
||||||
payments,
|
payments,
|
||||||
@ -119,6 +120,7 @@ pub fn proxy_bypass_urls(locker: &Locker) -> Vec<String> {
|
|||||||
format!("{locker_host_rs}/cards/add"),
|
format!("{locker_host_rs}/cards/add"),
|
||||||
format!("{locker_host_rs}/cards/retrieve"),
|
format!("{locker_host_rs}/cards/retrieve"),
|
||||||
format!("{locker_host_rs}/cards/delete"),
|
format!("{locker_host_rs}/cards/delete"),
|
||||||
|
format!("{locker_host_rs}{}", LOCKER_HEALTH_CALL_PATH),
|
||||||
format!("{locker_host}/card/addCard"),
|
format!("{locker_host}/card/addCard"),
|
||||||
format!("{locker_host}/card/getCard"),
|
format!("{locker_host}/card/getCard"),
|
||||||
format!("{locker_host}/card/deleteCard"),
|
format!("{locker_host}/card/deleteCard"),
|
||||||
|
|||||||
@ -376,3 +376,53 @@ pub enum ConnectorError {
|
|||||||
#[error("Missing 3DS redirection payload: {field_name}")]
|
#[error("Missing 3DS redirection payload: {field_name}")]
|
||||||
MissingConnectorRedirectionPayload { field_name: &'static str },
|
MissingConnectorRedirectionPayload { field_name: &'static str },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum HealthCheckDBError {
|
||||||
|
#[error("Error while connecting to database")]
|
||||||
|
DBError,
|
||||||
|
#[error("Error while writing to database")]
|
||||||
|
DBWriteError,
|
||||||
|
#[error("Error while reading element in the database")]
|
||||||
|
DBReadError,
|
||||||
|
#[error("Error while deleting element in the database")]
|
||||||
|
DBDeleteError,
|
||||||
|
#[error("Unpredictable error occurred")]
|
||||||
|
UnknownError,
|
||||||
|
#[error("Error in database transaction")]
|
||||||
|
TransactionError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<diesel::result::Error> for HealthCheckDBError {
|
||||||
|
fn from(error: diesel::result::Error) -> Self {
|
||||||
|
match error {
|
||||||
|
diesel::result::Error::DatabaseError(_, _) => Self::DBError,
|
||||||
|
|
||||||
|
diesel::result::Error::RollbackErrorOnCommit { .. }
|
||||||
|
| diesel::result::Error::RollbackTransaction
|
||||||
|
| diesel::result::Error::AlreadyInTransaction
|
||||||
|
| diesel::result::Error::NotInTransaction
|
||||||
|
| diesel::result::Error::BrokenTransactionManager => Self::TransactionError,
|
||||||
|
|
||||||
|
_ => Self::UnknownError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum HealthCheckRedisError {
|
||||||
|
#[error("Failed to establish Redis connection")]
|
||||||
|
RedisConnectionError,
|
||||||
|
#[error("Failed to set key value in Redis")]
|
||||||
|
SetFailed,
|
||||||
|
#[error("Failed to get key value in Redis")]
|
||||||
|
GetFailed,
|
||||||
|
#[error("Failed to delete key value in Redis")]
|
||||||
|
DeleteFailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, thiserror::Error)]
|
||||||
|
pub enum HealthCheckLockerError {
|
||||||
|
#[error("Failed to establish Locker connection")]
|
||||||
|
FailedToCallLocker,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user