Skip to content

Commit 14a8180

Browse files
committed
feat(model-server-lib): /health endpoint
https://issues.modelix.org/issue/MODELIX-411
1 parent 4eec68a commit 14a8180

File tree

1 file changed

+60
-1
lines changed

1 file changed

+60
-1
lines changed

model-server-lib/src/main/kotlin/org/modelix/model/server/light/LightModelServer.kt

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ import io.ktor.server.application.*
2121
import io.ktor.server.engine.*
2222
import io.ktor.server.netty.*
2323
import io.ktor.server.plugins.cors.routing.*
24+
import io.ktor.server.response.*
2425
import io.ktor.server.routing.*
2526
import io.ktor.server.websocket.*
27+
import io.ktor.util.*
2628
import io.ktor.websocket.*
2729
import kotlinx.coroutines.*
2830
import org.modelix.model.api.ConceptReference
@@ -54,14 +56,26 @@ import java.time.Duration
5456
import java.util.*
5557
import kotlin.time.Duration.Companion.seconds
5658

57-
class LightModelServer(val port: Int, val rootNode: INode, val ignoredRoles: Set<IRole> = emptySet()) {
59+
class LightModelServer(val port: Int, val rootNode: INode, val ignoredRoles: Set<IRole> = emptySet(), additionalHealthChecks: List<IHealthCheck>) {
60+
constructor(port: Int, rootNode: INode, ignoredRoles: Set<IRole> = emptySet()) : this(port, rootNode, ignoredRoles, emptyList())
61+
5862
companion object {
5963
private val LOG = mu.KotlinLogging.logger { }
6064
}
6165

6266
private var server: NettyApplicationEngine? = null
6367
private val sessions: MutableSet<SessionData> = Collections.synchronizedSet(HashSet())
6468
private val ignoredRolesCache: MutableMap<IConceptReference, IgnoredRoles> = HashMap()
69+
private val healthChecks: List<IHealthCheck> = listOf(object : IHealthCheck {
70+
override val id: String = "readRootNode"
71+
override val enabledByDefault: Boolean = true
72+
73+
override fun run(output: StringBuilder): Boolean {
74+
val count = getArea().executeRead { rootNode.allChildren.count() }
75+
output.appendLine("root node has $count children")
76+
return true
77+
}
78+
}) + additionalHealthChecks
6579

6680
fun start() {
6781
LOG.trace { "server starting on port $port ..." }
@@ -123,6 +137,45 @@ class LightModelServer(val port: Int, val rootNode: INode, val ignoredRoles: Set
123137
sessions.remove(session)
124138
}
125139
}
140+
get("/health") {
141+
val output = StringBuilder()
142+
try {
143+
val allChecks = healthChecks.associateBy { it.id }.toMap()
144+
val enabledChecks = allChecks.filter { it.value.enabledByDefault }.keys.toMutableSet()
145+
146+
call.request.queryParameters.entries().forEach { entry ->
147+
entry.value.forEach { value ->
148+
if (!allChecks.containsKey(entry.key)) throw IllegalArgumentException("Unknown check: ${entry.key}")
149+
if (value.toBooleanStrict()) {
150+
enabledChecks.add(entry.key)
151+
} else {
152+
enabledChecks.remove(entry.key)
153+
}
154+
}
155+
}
156+
var isHealthy = true
157+
for (healthCheck in allChecks.values) {
158+
if (enabledChecks.contains(healthCheck.id)) {
159+
output.appendLine("--- running check '${healthCheck.id}' ---")
160+
val result = healthCheck.run(output)
161+
output.appendLine()
162+
output.appendLine("-> " + if (result) "successful" else "failed")
163+
isHealthy = isHealthy && result
164+
} else {
165+
output.appendLine("--- check '${healthCheck.id}' is disabled. Use '/health?${healthCheck.id}=true' to enable it.")
166+
}
167+
}
168+
if (isHealthy) {
169+
call.respond(HttpStatusCode.OK, "healthy\n\n$output")
170+
} else {
171+
call.respond(HttpStatusCode.InternalServerError, "unhealthy\n\n$output")
172+
}
173+
} catch (ex: Exception) {
174+
output.appendLine()
175+
output.appendLine(ex.stackTraceToString())
176+
call.respond(HttpStatusCode.InternalServerError, "unhealthy\n\n$output")
177+
}
178+
}
126179
}
127180
install(CORS) {
128181
anyHost()
@@ -314,6 +367,12 @@ class LightModelServer(val port: Int, val rootNode: INode, val ignoredRoles: Set
314367
children = childrenMap,
315368
)
316369
}
370+
371+
interface IHealthCheck {
372+
val id: String
373+
val enabledByDefault: Boolean
374+
fun run(output: StringBuilder): Boolean
375+
}
317376
}
318377

319378
private class IgnoredRoles(val children: Set<String>, val properties: Set<String>, val references: Set<String>) {

0 commit comments

Comments
 (0)