@@ -21,8 +21,10 @@ import io.ktor.server.application.*
2121import io.ktor.server.engine.*
2222import io.ktor.server.netty.*
2323import io.ktor.server.plugins.cors.routing.*
24+ import io.ktor.server.response.*
2425import io.ktor.server.routing.*
2526import io.ktor.server.websocket.*
27+ import io.ktor.util.*
2628import io.ktor.websocket.*
2729import kotlinx.coroutines.*
2830import org.modelix.model.api.ConceptReference
@@ -54,14 +56,26 @@ import java.time.Duration
5456import java.util.*
5557import 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
319378private class IgnoredRoles (val children : Set <String >, val properties : Set <String >, val references : Set <String >) {
0 commit comments