Adding GET /system/connectors/stored_procedure_vDec2019/health

This commit is contained in:
simonredfern 2026-01-29 05:03:39 +01:00
parent 2731a4954b
commit dc53c9367b
6 changed files with 186 additions and 1 deletions

View File

@ -412,6 +412,9 @@ object ApiRole extends MdcLoggable{
case class CanGetDatabasePoolInfo(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetDatabasePoolInfo = CanGetDatabasePoolInfo()
case class CanGetConnectorHealth(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetConnectorHealth = CanGetConnectorHealth()
case class CanGetCacheNamespaces(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetCacheNamespaces = CanGetCacheNamespaces()

View File

@ -113,6 +113,7 @@ object ApiTag {
val apiTagJsonSchemaValidation = ResourceDocTag("JSON-Schema-Validation")
val apiTagAuthenticationTypeValidation = ResourceDocTag("Authentication-Type-Validation")
val apiTagConnectorMethod = ResourceDocTag("Connector-Method")
val apiTagConnector = ResourceDocTag("Connector")
// To mark the Berlin Group APIs suggested order of implementation
val apiTagBerlinGroupM = ResourceDocTag("Berlin-Group-M")

View File

@ -1854,7 +1854,7 @@ trait APIMethods310 {
"GET",
"/connector/loopback",
"Get Connector Status (Loopback)",
s"""This endpoint makes a call to the Connector to check the backend transport is reachable. (WIP)
s"""This endpoint makes a call to the Connector to check the backend transport is reachable. (Deprecated)
|
|${userAuthenticationMessage(true)}
|

View File

@ -35,6 +35,7 @@ import code.api.v6_0_0.OBPAPI6_0_0
import code.abacrule.{AbacRuleEngine, MappedAbacRuleProvider}
import code.metrics.APIMetrics
import code.bankconnectors.{Connector, LocalMappedConnectorInternal}
import code.bankconnectors.storedprocedure.StoredProcedureUtils
import code.bankconnectors.LocalMappedConnectorInternal._
import code.entitlement.Entitlement
import code.loginattempts.LoginAttempt
@ -851,6 +852,71 @@ trait APIMethods600 {
}
}
staticResourceDocs += ResourceDoc(
getStoredProcedureConnectorHealth,
implementedInApiVersion,
nameOf(getStoredProcedureConnectorHealth),
"GET",
"/system/connectors/stored_procedure_vDec2019/health",
"Get Stored Procedure Connector Health",
"""Returns health status of the stored procedure connector including:
|
|- Connection status (ok/error)
|- Database server name: identifies which backend node handled the request (useful for load balancer diagnostics)
|- Server IP address
|- Database name
|- Response time in milliseconds
|- Error message (if any)
|
|Supports database-specific queries for: SQL Server, PostgreSQL, Oracle, and MySQL/MariaDB.
|
|This endpoint is useful for diagnosing connectivity issues, especially when the database is behind a load balancer
|and you need to identify which node is responding or experiencing SSL certificate issues.
|
|Note: This endpoint may take a long time to respond if the database connection is slow or experiencing issues.
|The response time depends on the connection pool timeout and JDBC driver settings.
|
|Authentication is Required
|""",
EmptyBody,
StoredProcedureConnectorHealthJsonV600(
status = "ok",
server_name = Some("DBSERVER01"),
server_ip = Some("10.0.1.50"),
database_name = Some("obp_adapter"),
response_time_ms = 45,
error_message = None
),
List(
AuthenticatedUserIsRequired,
UserHasMissingRoles,
UnknownError
),
List(apiTagConnector, apiTagSystem, apiTagApi),
Some(List(canGetConnectorHealth))
)
lazy val getStoredProcedureConnectorHealth: OBPEndpoint = {
case "system" :: "connectors" :: "stored_procedure_vDec2019" :: "health" :: Nil JsonGet _ => {
cc => implicit val ec = EndpointContext(Some(cc))
for {
(Full(u), callContext) <- authenticatedAccess(cc)
_ <- NewStyle.function.hasEntitlement("", u.userId, canGetConnectorHealth, callContext)
} yield {
val health = StoredProcedureUtils.getHealth()
val result = StoredProcedureConnectorHealthJsonV600(
status = health.status,
server_name = health.serverName,
server_ip = health.serverIp,
database_name = health.databaseName,
response_time_ms = health.responseTimeMs,
error_message = health.errorMessage
)
(result, HttpCode.`200`(callContext))
}
}
}
lazy val getCurrentConsumer: OBPEndpoint = {
case "consumers" :: "current" :: Nil JsonGet _ => {
cc => {

View File

@ -325,6 +325,15 @@ case class DatabasePoolInfoJsonV600(
keepalive_time_ms: Long
)
case class StoredProcedureConnectorHealthJsonV600(
status: String,
server_name: Option[String],
server_ip: Option[String],
database_name: Option[String],
response_time_ms: Long,
error_message: Option[String]
)
case class PostCustomerJsonV600(
legal_name: String,
customer_number: Option[String] = None,

View File

@ -49,6 +49,112 @@ object StoredProcedureUtils extends MdcLoggable{
}
/**
* Health check case class for stored procedure connector
*/
case class StoredProcedureConnectorHealth(
status: String,
serverName: Option[String],
serverIp: Option[String],
databaseName: Option[String],
responseTimeMs: Long,
errorMessage: Option[String]
)
/**
* Perform a health check on the stored procedure connector.
* Executes a database-specific query to verify connectivity and identify the backend node.
* Supports: SQL Server, PostgreSQL, Oracle, and MySQL.
*/
def getHealth(): StoredProcedureConnectorHealth = {
val startTime = System.currentTimeMillis()
try {
val (serverName, serverIp, databaseName) = scalikeDB readOnly { implicit session =>
val driver = APIUtil.getPropsValue("stored_procedure_connector.driver", "")
if (driver.contains("sqlserver")) {
// Microsoft SQL Server
val result = sql"""
SELECT
@@SERVERNAME AS server_name,
CAST(CONNECTIONPROPERTY('local_net_address') AS VARCHAR(50)) AS server_ip,
DB_NAME() AS database_name
""".map(rs => (
Option(rs.string("server_name")),
Option(rs.string("server_ip")),
Option(rs.string("database_name"))
)).single.apply()
result.getOrElse((None, None, None))
} else if (driver.contains("postgresql")) {
// PostgreSQL
val result = sql"""
SELECT
inet_server_addr()::text AS server_ip,
current_database() AS database_name,
(SELECT setting FROM pg_settings WHERE name = 'cluster_name') AS server_name
""".map(rs => (
rs.stringOpt("server_name"),
rs.stringOpt("server_ip"),
rs.stringOpt("database_name")
)).single.apply()
result.getOrElse((None, None, None))
} else if (driver.contains("oracle")) {
// Oracle
val result = sql"""
SELECT
SYS_CONTEXT('USERENV', 'SERVER_HOST') AS server_name,
SYS_CONTEXT('USERENV', 'IP_ADDRESS') AS server_ip,
SYS_CONTEXT('USERENV', 'DB_NAME') AS database_name
FROM DUAL
""".map(rs => (
Option(rs.string("server_name")),
Option(rs.string("server_ip")),
Option(rs.string("database_name"))
)).single.apply()
result.getOrElse((None, None, None))
} else if (driver.contains("mysql") || driver.contains("mariadb")) {
// MySQL / MariaDB
val result = sql"""
SELECT
@@hostname AS server_name,
@@bind_address AS server_ip,
DATABASE() AS database_name
""".map(rs => (
Option(rs.string("server_name")),
Option(rs.string("server_ip")),
Option(rs.string("database_name"))
)).single.apply()
result.getOrElse((None, None, None))
} else {
// Generic fallback - just test connectivity
sql"SELECT 1".map(_ => ()).single.apply()
(None, None, None)
}
}
val responseTime = System.currentTimeMillis() - startTime
StoredProcedureConnectorHealth(
status = "ok",
serverName = serverName,
serverIp = serverIp,
databaseName = databaseName,
responseTimeMs = responseTime,
errorMessage = None
)
} catch {
case e: Exception =>
val responseTime = System.currentTimeMillis() - startTime
logger.error(s"Stored procedure connector health check failed: ${e.getMessage}", e)
StoredProcedureConnectorHealth(
status = "error",
serverName = None,
serverIp = None,
databaseName = None,
responseTimeMs = responseTime,
errorMessage = Some(e.getMessage)
)
}
}
def callProcedure[T: Manifest](procedureName: String, outBound: TopicTrait): Box[T] = {
val procedureParam: String = write(outBound) // convert OutBound to json string
logger.debug(s"${StoredProcedureConnector_vDec2019.toString} outBoundJson: $procedureName = $procedureParam" )