Skip to content
This repository has been archived by the owner on Sep 29, 2022. It is now read-only.

Commit

Permalink
Feature/aos 2839 helse endepunkt formater (#62)
Browse files Browse the repository at this point in the history
* Fixed issue where we needed to support several health endpoint formats

* Refactored, generalized

* Readability improvements
  • Loading branch information
bentsolheim committed Oct 1, 2018
1 parent 5620e8a commit 0f7ff6b
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,13 @@ enum class HealthStatus { UP, OBSERVE, COMMENT, UNKNOWN, OUT_OF_SERVICE, DOWN }

data class HealthResponse(
val status: HealthStatus,
val parts: MutableMap<String, HealthPart> = mutableMapOf()
) {
@JsonAnySetter(enabled = true)
private fun setAny(name: String, value: HealthPart) {
parts[name] = value
}
}
val parts: Map<String, HealthPart> = emptyMap()
)

data class HealthPart(val status: HealthStatus, val details: MutableMap<String, JsonNode> = mutableMapOf()) {
@JsonAnySetter(enabled = true)
private fun setAny(name: String, value: JsonNode) {
details[name] = value
}
}
data class HealthPart(
val status: HealthStatus=HealthStatus.UP,
val details: Map<String, JsonNode> = emptyMap()
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class InfoResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.exc.MismatchedInputException
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import no.skatteetaten.aurora.mokey.extensions.asMap
import no.skatteetaten.aurora.mokey.extensions.extract
import no.skatteetaten.aurora.mokey.model.Endpoint
import no.skatteetaten.aurora.mokey.model.Endpoint.ENV
import no.skatteetaten.aurora.mokey.model.Endpoint.HEALTH
import no.skatteetaten.aurora.mokey.model.Endpoint.INFO
import no.skatteetaten.aurora.mokey.model.HealthPart
import no.skatteetaten.aurora.mokey.model.HealthResponse
import no.skatteetaten.aurora.mokey.model.HealthStatus
import no.skatteetaten.aurora.mokey.model.HttpResponse
import no.skatteetaten.aurora.mokey.model.InfoResponse
import no.skatteetaten.aurora.mokey.model.ManagementLinks
Expand Down Expand Up @@ -40,7 +44,11 @@ class ManagementEndpoint internal constructor(
) {

@Throws(ManagementEndpointException::class)
fun getHealthEndpointResponse(): HttpResponse<HealthResponse> = findJsonResource(HEALTH, HealthResponse::class)
fun getHealthEndpointResponse(): HttpResponse<HealthResponse> {
val response = findJsonResource(HEALTH, JsonNode::class)
val healthResponse = HealthResponseParser.parse(response.deserialized)
return HttpResponse(healthResponse, response.textResponse, response.createdAt)
}

@Throws(ManagementEndpointException::class)
fun getInfoEndpointResponse(): HttpResponse<InfoResponse> = findJsonResource(INFO, InfoResponse::class)
Expand Down Expand Up @@ -101,4 +109,60 @@ class ManagementEndpoint internal constructor(
return HttpResponse(deserialized, jsonString)
}
}
}

object HealthResponseParser {

enum class HealthResponseFormat { SPRING_BOOT_1X, SPRING_BOOT_2X }

private const val STATUS_PROPERTY = "status"
private const val DETAILS_PROPERTY = "details"

fun parse(json: JsonNode): HealthResponse = when (json.format) {
HealthResponseFormat.SPRING_BOOT_2X -> handleSpringBoot2Format(json)
else -> handleSpringBoot1Format(json)
}

private fun handleSpringBoot2Format(json: JsonNode): HealthResponse =
handleSpringBootFormat(json) { it.details }

private fun handleSpringBoot1Format(json: JsonNode): HealthResponse =
handleSpringBootFormat(json) { it.allNodesExceptStatus }

private fun handleSpringBootFormat(
json: JsonNode,
detailsExtractor: (JsonNode) -> Map<String, JsonNode>
): HealthResponse {
val healthStatus = json.status
val allDetails = detailsExtractor(json)
val parts = allDetails.mapValues {
val status = it.value.status
val partDetails = detailsExtractor(it.value)
HealthPart(status, partDetails)
}
return HealthResponse(healthStatus, parts)
}

private val JsonNode.format
get(): HealthResponseFormat {
return if (this.has(DETAILS_PROPERTY) && this.has(STATUS_PROPERTY) && this.size() == 2) {
HealthResponseFormat.SPRING_BOOT_2X
} else {
HealthResponseFormat.SPRING_BOOT_1X
}
}

private val JsonNode.details get() = this.extract("/$DETAILS_PROPERTY").asMap()

private val JsonNode.allNodesExceptStatus
get() = this.asMap().toMutableMap().also { it.remove(STATUS_PROPERTY) }.toMap()

private val JsonNode.status
get(): HealthStatus {
return try {
this.extract("/$STATUS_PROPERTY")?.textValue()?.let { HealthStatus.valueOf(it) }
} catch (e: Throwable) {
null
} ?: throw IllegalArgumentException("Element did not contain valid $STATUS_PROPERTY property")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package no.skatteetaten.aurora.mokey.service

import assertk.assert
import assertk.assertions.isEqualTo
import com.fasterxml.jackson.databind.node.IntNode
import com.fasterxml.jackson.databind.node.LongNode
import com.fasterxml.jackson.databind.node.TextNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import no.skatteetaten.aurora.mokey.model.HealthPart
import no.skatteetaten.aurora.mokey.model.HealthResponse
import no.skatteetaten.aurora.mokey.model.HealthStatus
import org.intellij.lang.annotations.Language
import org.junit.jupiter.api.Test

class HealthResponseParserTest {

@Test
fun `parse flat spring-boot 1-X health endpoint response`() {
@Language("JSON")
val json = """{
"status": "UP",
"atsServiceHelse": {
"status": "UP"
},
"diskSpace": {
"status": "UP",
"total": 10718543872,
"free": 10508611584,
"threshold": 10485760
},
"db": {
"status": "UP",
"database": "Oracle",
"hello": "Hello"
}
}"""

val response = parse(json)
val expected = HealthResponse(
status = HealthStatus.UP,
parts = mutableMapOf(
"atsServiceHelse" to HealthPart(
status = HealthStatus.UP,
details = mutableMapOf()
),
"diskSpace" to HealthPart(
status = HealthStatus.UP,
details = mutableMapOf(
"total" to 10718543872.node(),
"threshold" to 10485760.node(),
"free" to 10508611584.node()
)
),
"db" to HealthPart(
status = HealthStatus.UP,
details = mutableMapOf(
"hello" to "Hello".node(),
"database" to "Oracle".node()
)
)
)
)
assert(response).isEqualTo(expected)
}

@Test
fun `parse nested spring-boot 2-X health endpoint response`() {
@Language("JSON")
val json = """{
"status": "UP",
"details": {
"diskSpace": {
"status": "UP",
"details": {
"total": 10718543872,
"free": 10502053888,
"threshold": 10485760
}
},
"db": {
"status": "UP",
"details": {
"database": "Oracle",
"hello": "Hello"
}
}
}
}"""

val response = parse(json)
val expected = HealthResponse(
status = HealthStatus.UP,
parts = mutableMapOf(
"diskSpace" to HealthPart(
status = HealthStatus.UP,
details = mutableMapOf(
"total" to 10718543872.node(),
"threshold" to 10485760.node(),
"free" to 10502053888.node()
)
),
"db" to HealthPart(
status = HealthStatus.UP,
details = mutableMapOf(
"hello" to "Hello".node(),
"database" to "Oracle".node()
)
)
)
)
assert(response).isEqualTo(expected)
}

private fun String.node() = TextNode.valueOf(this)!!

private fun Int.node() = IntNode.valueOf(this)!!

private fun Long.node() = LongNode.valueOf(this)!!

private fun parse(json: String) = HealthResponseParser.parse(jacksonObjectMapper().readTree(json))
}

0 comments on commit 0f7ff6b

Please sign in to comment.