Skip to content

Commit dc53c93

Browse files
committed
Adding GET /system/connectors/stored_procedure_vDec2019/health
1 parent 2731a49 commit dc53c93

File tree

6 files changed

+186
-1
lines changed

6 files changed

+186
-1
lines changed

obp-api/src/main/scala/code/api/util/ApiRole.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,9 @@ object ApiRole extends MdcLoggable{
412412
case class CanGetDatabasePoolInfo(requiresBankId: Boolean = false) extends ApiRole
413413
lazy val canGetDatabasePoolInfo = CanGetDatabasePoolInfo()
414414

415+
case class CanGetConnectorHealth(requiresBankId: Boolean = false) extends ApiRole
416+
lazy val canGetConnectorHealth = CanGetConnectorHealth()
417+
415418

416419
case class CanGetCacheNamespaces(requiresBankId: Boolean = false) extends ApiRole
417420
lazy val canGetCacheNamespaces = CanGetCacheNamespaces()

obp-api/src/main/scala/code/api/util/ApiTag.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ object ApiTag {
113113
val apiTagJsonSchemaValidation = ResourceDocTag("JSON-Schema-Validation")
114114
val apiTagAuthenticationTypeValidation = ResourceDocTag("Authentication-Type-Validation")
115115
val apiTagConnectorMethod = ResourceDocTag("Connector-Method")
116+
val apiTagConnector = ResourceDocTag("Connector")
116117

117118
// To mark the Berlin Group APIs suggested order of implementation
118119
val apiTagBerlinGroupM = ResourceDocTag("Berlin-Group-M")

obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1854,7 +1854,7 @@ trait APIMethods310 {
18541854
"GET",
18551855
"/connector/loopback",
18561856
"Get Connector Status (Loopback)",
1857-
s"""This endpoint makes a call to the Connector to check the backend transport is reachable. (WIP)
1857+
s"""This endpoint makes a call to the Connector to check the backend transport is reachable. (Deprecated)
18581858
|
18591859
|${userAuthenticationMessage(true)}
18601860
|

obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import code.api.v6_0_0.OBPAPI6_0_0
3535
import code.abacrule.{AbacRuleEngine, MappedAbacRuleProvider}
3636
import code.metrics.APIMetrics
3737
import code.bankconnectors.{Connector, LocalMappedConnectorInternal}
38+
import code.bankconnectors.storedprocedure.StoredProcedureUtils
3839
import code.bankconnectors.LocalMappedConnectorInternal._
3940
import code.entitlement.Entitlement
4041
import code.loginattempts.LoginAttempt
@@ -851,6 +852,71 @@ trait APIMethods600 {
851852
}
852853
}
853854

855+
staticResourceDocs += ResourceDoc(
856+
getStoredProcedureConnectorHealth,
857+
implementedInApiVersion,
858+
nameOf(getStoredProcedureConnectorHealth),
859+
"GET",
860+
"/system/connectors/stored_procedure_vDec2019/health",
861+
"Get Stored Procedure Connector Health",
862+
"""Returns health status of the stored procedure connector including:
863+
|
864+
|- Connection status (ok/error)
865+
|- Database server name: identifies which backend node handled the request (useful for load balancer diagnostics)
866+
|- Server IP address
867+
|- Database name
868+
|- Response time in milliseconds
869+
|- Error message (if any)
870+
|
871+
|Supports database-specific queries for: SQL Server, PostgreSQL, Oracle, and MySQL/MariaDB.
872+
|
873+
|This endpoint is useful for diagnosing connectivity issues, especially when the database is behind a load balancer
874+
|and you need to identify which node is responding or experiencing SSL certificate issues.
875+
|
876+
|Note: This endpoint may take a long time to respond if the database connection is slow or experiencing issues.
877+
|The response time depends on the connection pool timeout and JDBC driver settings.
878+
|
879+
|Authentication is Required
880+
|""",
881+
EmptyBody,
882+
StoredProcedureConnectorHealthJsonV600(
883+
status = "ok",
884+
server_name = Some("DBSERVER01"),
885+
server_ip = Some("10.0.1.50"),
886+
database_name = Some("obp_adapter"),
887+
response_time_ms = 45,
888+
error_message = None
889+
),
890+
List(
891+
AuthenticatedUserIsRequired,
892+
UserHasMissingRoles,
893+
UnknownError
894+
),
895+
List(apiTagConnector, apiTagSystem, apiTagApi),
896+
Some(List(canGetConnectorHealth))
897+
)
898+
899+
lazy val getStoredProcedureConnectorHealth: OBPEndpoint = {
900+
case "system" :: "connectors" :: "stored_procedure_vDec2019" :: "health" :: Nil JsonGet _ => {
901+
cc => implicit val ec = EndpointContext(Some(cc))
902+
for {
903+
(Full(u), callContext) <- authenticatedAccess(cc)
904+
_ <- NewStyle.function.hasEntitlement("", u.userId, canGetConnectorHealth, callContext)
905+
} yield {
906+
val health = StoredProcedureUtils.getHealth()
907+
val result = StoredProcedureConnectorHealthJsonV600(
908+
status = health.status,
909+
server_name = health.serverName,
910+
server_ip = health.serverIp,
911+
database_name = health.databaseName,
912+
response_time_ms = health.responseTimeMs,
913+
error_message = health.errorMessage
914+
)
915+
(result, HttpCode.`200`(callContext))
916+
}
917+
}
918+
}
919+
854920
lazy val getCurrentConsumer: OBPEndpoint = {
855921
case "consumers" :: "current" :: Nil JsonGet _ => {
856922
cc => {

obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,15 @@ case class DatabasePoolInfoJsonV600(
325325
keepalive_time_ms: Long
326326
)
327327

328+
case class StoredProcedureConnectorHealthJsonV600(
329+
status: String,
330+
server_name: Option[String],
331+
server_ip: Option[String],
332+
database_name: Option[String],
333+
response_time_ms: Long,
334+
error_message: Option[String]
335+
)
336+
328337
case class PostCustomerJsonV600(
329338
legal_name: String,
330339
customer_number: Option[String] = None,

obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureUtils.scala

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,112 @@ object StoredProcedureUtils extends MdcLoggable{
4949
}
5050

5151

52+
/**
53+
* Health check case class for stored procedure connector
54+
*/
55+
case class StoredProcedureConnectorHealth(
56+
status: String,
57+
serverName: Option[String],
58+
serverIp: Option[String],
59+
databaseName: Option[String],
60+
responseTimeMs: Long,
61+
errorMessage: Option[String]
62+
)
63+
64+
/**
65+
* Perform a health check on the stored procedure connector.
66+
* Executes a database-specific query to verify connectivity and identify the backend node.
67+
* Supports: SQL Server, PostgreSQL, Oracle, and MySQL.
68+
*/
69+
def getHealth(): StoredProcedureConnectorHealth = {
70+
val startTime = System.currentTimeMillis()
71+
try {
72+
val (serverName, serverIp, databaseName) = scalikeDB readOnly { implicit session =>
73+
val driver = APIUtil.getPropsValue("stored_procedure_connector.driver", "")
74+
75+
if (driver.contains("sqlserver")) {
76+
// Microsoft SQL Server
77+
val result = sql"""
78+
SELECT
79+
@@SERVERNAME AS server_name,
80+
CAST(CONNECTIONPROPERTY('local_net_address') AS VARCHAR(50)) AS server_ip,
81+
DB_NAME() AS database_name
82+
""".map(rs => (
83+
Option(rs.string("server_name")),
84+
Option(rs.string("server_ip")),
85+
Option(rs.string("database_name"))
86+
)).single.apply()
87+
result.getOrElse((None, None, None))
88+
} else if (driver.contains("postgresql")) {
89+
// PostgreSQL
90+
val result = sql"""
91+
SELECT
92+
inet_server_addr()::text AS server_ip,
93+
current_database() AS database_name,
94+
(SELECT setting FROM pg_settings WHERE name = 'cluster_name') AS server_name
95+
""".map(rs => (
96+
rs.stringOpt("server_name"),
97+
rs.stringOpt("server_ip"),
98+
rs.stringOpt("database_name")
99+
)).single.apply()
100+
result.getOrElse((None, None, None))
101+
} else if (driver.contains("oracle")) {
102+
// Oracle
103+
val result = sql"""
104+
SELECT
105+
SYS_CONTEXT('USERENV', 'SERVER_HOST') AS server_name,
106+
SYS_CONTEXT('USERENV', 'IP_ADDRESS') AS server_ip,
107+
SYS_CONTEXT('USERENV', 'DB_NAME') AS database_name
108+
FROM DUAL
109+
""".map(rs => (
110+
Option(rs.string("server_name")),
111+
Option(rs.string("server_ip")),
112+
Option(rs.string("database_name"))
113+
)).single.apply()
114+
result.getOrElse((None, None, None))
115+
} else if (driver.contains("mysql") || driver.contains("mariadb")) {
116+
// MySQL / MariaDB
117+
val result = sql"""
118+
SELECT
119+
@@hostname AS server_name,
120+
@@bind_address AS server_ip,
121+
DATABASE() AS database_name
122+
""".map(rs => (
123+
Option(rs.string("server_name")),
124+
Option(rs.string("server_ip")),
125+
Option(rs.string("database_name"))
126+
)).single.apply()
127+
result.getOrElse((None, None, None))
128+
} else {
129+
// Generic fallback - just test connectivity
130+
sql"SELECT 1".map(_ => ()).single.apply()
131+
(None, None, None)
132+
}
133+
}
134+
val responseTime = System.currentTimeMillis() - startTime
135+
StoredProcedureConnectorHealth(
136+
status = "ok",
137+
serverName = serverName,
138+
serverIp = serverIp,
139+
databaseName = databaseName,
140+
responseTimeMs = responseTime,
141+
errorMessage = None
142+
)
143+
} catch {
144+
case e: Exception =>
145+
val responseTime = System.currentTimeMillis() - startTime
146+
logger.error(s"Stored procedure connector health check failed: ${e.getMessage}", e)
147+
StoredProcedureConnectorHealth(
148+
status = "error",
149+
serverName = None,
150+
serverIp = None,
151+
databaseName = None,
152+
responseTimeMs = responseTime,
153+
errorMessage = Some(e.getMessage)
154+
)
155+
}
156+
}
157+
52158
def callProcedure[T: Manifest](procedureName: String, outBound: TopicTrait): Box[T] = {
53159
val procedureParam: String = write(outBound) // convert OutBound to json string
54160
logger.debug(s"${StoredProcedureConnector_vDec2019.toString} outBoundJson: $procedureName = $procedureParam" )

0 commit comments

Comments
 (0)