@@ -21,8 +21,10 @@ import io.ktor.server.application.*
21
21
import io.ktor.server.engine.*
22
22
import io.ktor.server.netty.*
23
23
import io.ktor.server.plugins.cors.routing.*
24
+ import io.ktor.server.response.*
24
25
import io.ktor.server.routing.*
25
26
import io.ktor.server.websocket.*
27
+ import io.ktor.util.*
26
28
import io.ktor.websocket.*
27
29
import kotlinx.coroutines.*
28
30
import org.modelix.model.api.ConceptReference
@@ -54,14 +56,26 @@ import java.time.Duration
54
56
import java.util.*
55
57
import kotlin.time.Duration.Companion.seconds
56
58
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
+
58
62
companion object {
59
63
private val LOG = mu.KotlinLogging .logger { }
60
64
}
61
65
62
66
private var server: NettyApplicationEngine ? = null
63
67
private val sessions: MutableSet <SessionData > = Collections .synchronizedSet(HashSet ())
64
68
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
65
79
66
80
fun start () {
67
81
LOG .trace { " server starting on port $port ..." }
@@ -123,6 +137,45 @@ class LightModelServer(val port: Int, val rootNode: INode, val ignoredRoles: Set
123
137
sessions.remove(session)
124
138
}
125
139
}
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
+ }
126
179
}
127
180
install(CORS ) {
128
181
anyHost()
@@ -314,6 +367,12 @@ class LightModelServer(val port: Int, val rootNode: INode, val ignoredRoles: Set
314
367
children = childrenMap,
315
368
)
316
369
}
370
+
371
+ interface IHealthCheck {
372
+ val id: String
373
+ val enabledByDefault: Boolean
374
+ fun run (output : StringBuilder ): Boolean
375
+ }
317
376
}
318
377
319
378
private class IgnoredRoles (val children : Set <String >, val properties : Set <String >, val references : Set <String >) {
0 commit comments