@@ -28,7 +28,23 @@ import org.slf4j.LoggerFactory
2828import java .time .Instant
2929import scala .concurrent .duration ._
3030
31- case class StatusCheck (name : String , ok : Boolean )
31+ /** Diagnostic detail for a status check. Populated when the check exercised a
32+ * remote endpoint or otherwise has actionable failure information to show.
33+ */
34+ case class CheckDetail (
35+ url : Option [String ] = None ,
36+ method : Option [String ] = None ,
37+ requestBody : Option [String ] = None ,
38+ responseStatus : Option [Int ] = None ,
39+ responseBody : Option [String ] = None ,
40+ error : Option [String ] = None
41+ )
42+
43+ case class StatusCheck (
44+ name : String ,
45+ ok : Boolean ,
46+ detail : Option [CheckDetail ] = None
47+ )
3248
3349case class StatusReport (
3450 overallOk : Boolean ,
@@ -37,15 +53,31 @@ case class StatusReport(
3753)
3854
3955object StatusReport {
56+ private def detailToJson (d : CheckDetail ): Json = Json .obj(
57+ List (
58+ d.url.map(" url" -> Json .fromString(_)),
59+ d.method.map(" method" -> Json .fromString(_)),
60+ d.requestBody.map(" request_body" -> Json .fromString(_)),
61+ d.responseStatus.map(s => " response_status" -> Json .fromInt(s)),
62+ d.responseBody.map(" response_body" -> Json .fromString(_)),
63+ d.error.map(" error" -> Json .fromString(_))
64+ ).flatten: _*
65+ )
66+
4067 def toJson (report : StatusReport ): Json = Json .obj(
4168 " status" -> Json .fromString(if (report.overallOk) " ok" else " fail" ),
4269 " generated_at" -> Json .fromString(report.generatedAt.toString),
4370 " checks" -> Json .arr(
4471 report.checks.map { c =>
45- Json .obj (
72+ val base = List (
4673 " name" -> Json .fromString(c.name),
4774 " ok" -> Json .fromBoolean(c.ok)
4875 )
76+ val withDetail = c.detail match {
77+ case Some (d) => base :+ (" detail" -> detailToJson(d))
78+ case None => base
79+ }
80+ Json .obj(withDetail : _* )
4981 }: _*
5082 )
5183 )
@@ -94,9 +126,9 @@ class StatusService(
94126
95127 // Authentication + probes require a DirectLogin token
96128 val tokenAndProbesIO : IO [List [StatusCheck ]] = obtainToken().flatMap {
97- case Left (_ ) =>
129+ case Left (err ) =>
98130 // OBP API authentication failed. Mark all dependent checks as FAIL.
99- IO .pure(dependentFailChecks())
131+ IO .pure(dependentFailChecks(err ))
100132 case Right (token) =>
101133 val authOk = StatusCheck (" OBP API authentication" , ok = true )
102134 val rolesIO = checkRoles()
@@ -125,26 +157,29 @@ class StatusService(
125157 } yield StatusReport (overall, all, now)
126158 }
127159
128- private def dependentFailChecks (): List [StatusCheck ] = {
160+ private def dependentFailChecks (authError : String ): List [StatusCheck ] = {
161+ val skipped =
162+ Some (CheckDetail (error = Some (s " Skipped — OBP API authentication failed: $authError" )))
163+ val authDetail = Some (CheckDetail (error = Some (authError)))
129164 val base = List (
130- StatusCheck (" OBP API authentication" , ok = false ),
131- StatusCheck (" OBP API roles" , ok = false ),
132- StatusCheck (" Providers endpoint" , ok = false )
165+ StatusCheck (" OBP API authentication" , ok = false , detail = authDetail ),
166+ StatusCheck (" OBP API roles" , ok = false , detail = skipped ),
167+ StatusCheck (" Providers endpoint" , ok = false , detail = skipped )
133168 )
134169 val credential =
135170 if (config.verifyCredentialsMethod == VerifyCredentialsMethod .ViaApiEndpoint )
136171 List (
137- StatusCheck (" Credential verification endpoint" , ok = false ),
138- StatusCheck (" User lookup endpoint" , ok = false )
172+ StatusCheck (" Credential verification endpoint" , ok = false , detail = skipped ),
173+ StatusCheck (" User lookup endpoint" , ok = false , detail = skipped )
139174 )
140175 else Nil
141176 val clientVerify =
142177 if (config.verifyClientMethod == VerifyClientMethod .ViaApiEndpoint )
143- List (StatusCheck (" Client verification endpoint" , ok = false ))
178+ List (StatusCheck (" Client verification endpoint" , ok = false , detail = skipped ))
144179 else Nil
145180 val dcr =
146181 if (config.enableDynamicClientRegistration)
147- List (StatusCheck (" Consumer management endpoint" , ok = false ))
182+ List (StatusCheck (" Consumer management endpoint" , ok = false , detail = skipped ))
148183 else Nil
149184 base ++ credential ++ clientVerify ++ dcr
150185 }
@@ -173,18 +208,59 @@ class StatusService(
173208 private def checkObpReachable (): IO [StatusCheck ] = {
174209 val name = " OBP API reachable"
175210 config.obpApiUrl match {
176- case None => IO .pure(StatusCheck (name, ok = false ))
211+ case None =>
212+ IO .pure(
213+ StatusCheck (
214+ name,
215+ ok = false ,
216+ detail = Some (CheckDetail (error = Some (" OBP_API_URL not configured" )))
217+ )
218+ )
177219 case Some (baseUrl) =>
178220 val endpoint = s " ${baseUrl.stripSuffix(" /" )}/obp/v4.0.0/root "
179221 val req = Request [IO ](Method .GET , Uri .unsafeFromString(endpoint))
180222 client
181223 .run(req)
182- .use { resp => IO .pure(resp.status.isSuccess) }
183- .handleError(_ => false )
184- .map(ok => StatusCheck (name, ok))
224+ .use { resp =>
225+ val code = resp.status.code
226+ resp.as[String ].attempt.map(_.toOption.map(truncateBody)).map { body =>
227+ val ok = resp.status.isSuccess
228+ val detail =
229+ if (ok) None
230+ else
231+ Some (
232+ CheckDetail (
233+ url = Some (endpoint),
234+ method = Some (" GET" ),
235+ responseStatus = Some (code),
236+ responseBody = body
237+ )
238+ )
239+ StatusCheck (name, ok, detail = detail)
240+ }
241+ }
242+ .handleError { e =>
243+ StatusCheck (
244+ name,
245+ ok = false ,
246+ detail = Some (
247+ CheckDetail (
248+ url = Some (endpoint),
249+ method = Some (" GET" ),
250+ error = Some (Option (e.getMessage).getOrElse(e.toString))
251+ )
252+ )
253+ )
254+ }
185255 }
186256 }
187257
258+ private val MaxBodyChars = 2000
259+
260+ private def truncateBody (s : String ): String =
261+ if (s.length <= MaxBodyChars ) s
262+ else s.take(MaxBodyChars ) + s " ... [truncated, ${s.length - MaxBodyChars } more chars] "
263+
188264 private def checkRoles (): IO [List [StatusCheck ]] = {
189265 val requiredRoles =
190266 (config.verifyCredentialsMethod match {
@@ -233,7 +309,8 @@ class StatusService(
233309 token : String ,
234310 body : Option [Json ]
235311 ): IO [StatusCheck ] = {
236- val uri = Uri .unsafeFromString(s " ${baseUrl.stripSuffix(" /" )}$path" )
312+ val fullUrl = s " ${baseUrl.stripSuffix(" /" )}$path"
313+ val uri = Uri .unsafeFromString(fullUrl)
237314 val base = Request [IO ](method, uri).putHeaders(
238315 Header .Raw (ci " DirectLogin " , s " token= $token" )
239316 )
@@ -242,21 +319,48 @@ class StatusService(
242319 base.withEntity(json).putHeaders(`Content-Type`(MediaType .application.json))
243320 case None => base
244321 }
322+ val requestBodyStr = body.map(_.noSpaces)
245323 client
246324 .run(req)
247325 .use { resp =>
248326 val code = resp.status.code
249- if (code >= 500 ) IO .pure(StatusCheck (name, ok = false ))
250- else if (code == 404 ) {
251- resp.as[String ].map { responseBody =>
252- val routeMissing =
253- responseBody.contains(" OBP-10404" ) ||
254- ! responseBody.contains(" OBP-" )
255- StatusCheck (name, ok = ! routeMissing)
256- }
257- } else IO .pure(StatusCheck (name, ok = true ))
327+ resp.as[String ].attempt.map(_.toOption.map(truncateBody)).map { respBody =>
328+ val ok =
329+ if (code >= 500 ) false
330+ else if (code == 404 ) {
331+ val raw = respBody.getOrElse(" " )
332+ val routeMissing = raw.contains(" OBP-10404" ) || ! raw.contains(" OBP-" )
333+ ! routeMissing
334+ } else true
335+ val detail =
336+ if (ok) None
337+ else
338+ Some (
339+ CheckDetail (
340+ url = Some (fullUrl),
341+ method = Some (method.name),
342+ requestBody = requestBodyStr,
343+ responseStatus = Some (code),
344+ responseBody = respBody
345+ )
346+ )
347+ StatusCheck (name, ok, detail = detail)
348+ }
349+ }
350+ .handleError { e =>
351+ StatusCheck (
352+ name,
353+ ok = false ,
354+ detail = Some (
355+ CheckDetail (
356+ url = Some (fullUrl),
357+ method = Some (method.name),
358+ requestBody = requestBodyStr,
359+ error = Some (Option (e.getMessage).getOrElse(e.toString))
360+ )
361+ )
362+ )
258363 }
259- .handleError(_ => StatusCheck (name, ok = false ))
260364 }
261365
262366 private def endpointProbes (token : String ): IO [List [StatusCheck ]] = {
0 commit comments