@@ -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,25 @@ 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 @JvmOverloads constructor (val port : Int , val rootNode : INode , val ignoredRoles : Set <IRole > = emptySet(), additionalHealthChecks : List <IHealthCheck > = emptyList()) {
60
+
58
61
companion object {
59
62
private val LOG = mu.KotlinLogging .logger { }
60
63
}
61
64
62
65
private var server: NettyApplicationEngine ? = null
63
66
private val sessions: MutableSet <SessionData > = Collections .synchronizedSet(HashSet ())
64
67
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
65
78
66
79
fun start () {
67
80
LOG .trace { " server starting on port $port ..." }
@@ -123,6 +136,45 @@ class LightModelServer(val port: Int, val rootNode: INode, val ignoredRoles: Set
123
136
sessions.remove(session)
124
137
}
125
138
}
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
+ }
126
178
}
127
179
install(CORS ) {
128
180
anyHost()
@@ -314,6 +366,12 @@ class LightModelServer(val port: Int, val rootNode: INode, val ignoredRoles: Set
314
366
children = childrenMap,
315
367
)
316
368
}
369
+
370
+ interface IHealthCheck {
371
+ val id: String
372
+ val enabledByDefault: Boolean
373
+ fun run (output : StringBuilder ): Boolean
374
+ }
317
375
}
318
376
319
377
private class IgnoredRoles (val children : Set <String >, val properties : Set <String >, val references : Set <String >) {
0 commit comments