@@ -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,25 @@ 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 @JvmOverloads constructor (val port : Int , val rootNode : INode , val ignoredRoles : Set <IRole > = emptySet(), additionalHealthChecks : List <IHealthCheck > = emptyList()) {
60+
5861 companion object {
5962 private val LOG = mu.KotlinLogging .logger { }
6063 }
6164
6265 private var server: NettyApplicationEngine ? = null
6366 private val sessions: MutableSet <SessionData > = Collections .synchronizedSet(HashSet ())
6467 private val ignoredRolesCache: MutableMap <IConceptReference , IgnoredRoles > = HashMap ()
68+ private val healthChecks: List <IHealthCheck > = listOf (object : IHealthCheck {
69+ override val id: String = " readRootNode"
70+ override val enabledByDefault: Boolean = true
71+
72+ override fun run (output : StringBuilder ): Boolean {
73+ val count = getArea().executeRead { rootNode.allChildren.count() }
74+ output.appendLine(" root node has $count children" )
75+ return true
76+ }
77+ }) + additionalHealthChecks
6578
6679 fun start () {
6780 LOG .trace { " server starting on port $port ..." }
@@ -123,6 +136,45 @@ class LightModelServer(val port: Int, val rootNode: INode, val ignoredRoles: Set
123136 sessions.remove(session)
124137 }
125138 }
139+ get(" /health" ) {
140+ val output = StringBuilder ()
141+ try {
142+ val allChecks = healthChecks.associateBy { it.id }.toMap()
143+ val enabledChecks = allChecks.filter { it.value.enabledByDefault }.keys.toMutableSet()
144+
145+ call.request.queryParameters.entries().forEach { entry ->
146+ entry.value.forEach { value ->
147+ if (! allChecks.containsKey(entry.key)) throw IllegalArgumentException (" Unknown check: ${entry.key} " )
148+ if (value.toBooleanStrict()) {
149+ enabledChecks.add(entry.key)
150+ } else {
151+ enabledChecks.remove(entry.key)
152+ }
153+ }
154+ }
155+ var isHealthy = true
156+ for (healthCheck in allChecks.values) {
157+ if (enabledChecks.contains(healthCheck.id)) {
158+ output.appendLine(" --- running check '${healthCheck.id} ' ---" )
159+ val result = healthCheck.run (output)
160+ output.appendLine()
161+ output.appendLine(" -> " + if (result) " successful" else " failed" )
162+ isHealthy = isHealthy && result
163+ } else {
164+ output.appendLine(" --- check '${healthCheck.id} ' is disabled. Use '/health?${healthCheck.id} =true' to enable it." )
165+ }
166+ }
167+ if (isHealthy) {
168+ call.respond(HttpStatusCode .OK , " healthy\n\n $output " )
169+ } else {
170+ call.respond(HttpStatusCode .InternalServerError , " unhealthy\n\n $output " )
171+ }
172+ } catch (ex: Exception ) {
173+ output.appendLine()
174+ output.appendLine(ex.stackTraceToString())
175+ call.respond(HttpStatusCode .InternalServerError , " unhealthy\n\n $output " )
176+ }
177+ }
126178 }
127179 install(CORS ) {
128180 anyHost()
@@ -314,6 +366,12 @@ class LightModelServer(val port: Int, val rootNode: INode, val ignoredRoles: Set
314366 children = childrenMap,
315367 )
316368 }
369+
370+ interface IHealthCheck {
371+ val id: String
372+ val enabledByDefault: Boolean
373+ fun run (output : StringBuilder ): Boolean
374+ }
317375}
318376
319377private class IgnoredRoles (val children : Set <String >, val properties : Set <String >, val references : Set <String >) {
0 commit comments