solution: health check endpoint

fix: #111
This commit is contained in:
Igor Artamonov
2021-10-20 23:10:41 -04:00
parent a04c1f3e66
commit e205985da3
13 changed files with 471 additions and 7 deletions

View File

@@ -16,12 +16,7 @@
*/
package io.emeraldpay.dshackle
import io.emeraldpay.dshackle.config.CacheConfig
import io.emeraldpay.dshackle.config.MainConfig
import io.emeraldpay.dshackle.config.MainConfigReader
import io.emeraldpay.dshackle.config.MonitoringConfig
import io.emeraldpay.dshackle.config.TokensConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.config.*
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
@@ -120,4 +115,9 @@ open class Config(
open fun monitoringConfig(@Autowired mainConfig: MainConfig): MonitoringConfig {
return mainConfig.monitoring
}
@Bean
open fun healthConfig(@Autowired mainConfig: MainConfig): HealthConfig {
return mainConfig.health
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.config
import io.emeraldpay.grpc.Chain
class HealthConfig {
companion object {
fun default(): HealthConfig {
return HealthConfig()
}
}
var port: Int = 8082
var host: String = "127.0.0.1"
var path: String = "/health"
val chains = HashMap<Chain, ChainConfig>()
fun isEnabled(): Boolean {
return chains.isNotEmpty()
}
fun configs(): Collection<ChainConfig> {
return chains.values
}
data class ChainConfig(
val blockchain: Chain,
val minAvailable: Int = 1
)
}

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.config
import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory
import org.yaml.snakeyaml.nodes.CollectionNode
import org.yaml.snakeyaml.nodes.MappingNode
import java.io.InputStream
class HealthConfigReader : YamlConfigReader(), ConfigReader<HealthConfig> {
companion object {
private val log = LoggerFactory.getLogger(HealthConfigReader::class.java)
}
fun read(input: InputStream): HealthConfig {
val configNode = readNode(input)
return read(configNode)
}
override fun read(input: MappingNode?): HealthConfig {
return readInternal(getMapping(input, "health"))
}
fun readInternal(input: MappingNode?): HealthConfig {
if (input == null) {
return HealthConfig.default()
}
val config = HealthConfig()
getValueAsString(input, "host")?.let {
config.host = it
}
getValueAsInt(input, "port")?.let {
config.port = it
}
getValueAsString(input, "path")?.let {
config.path = it
}
readBlockchains(config, getList<MappingNode>(input, "blockchains"))
return config
}
fun readBlockchains(healthConfig: HealthConfig, input: CollectionNode<MappingNode>?) {
if (input == null) {
return
}
input.value.forEach { conf ->
val chain = getValueAsString(conf, "chain")
?.let { getBlockchain(it) }
if (chain == null) {
log.warn("Blockchain is not specified for a Health Check")
return@forEach
}
if (chain == Chain.UNSPECIFIED) {
log.warn("Using UNSPECIFIED blockchain for Health Check. Always fails")
}
if (healthConfig.chains.containsKey(chain)) {
log.warn("Duplicate Health Check config for $chain. Replace previous with new")
}
val minAvailable = getValueAsInt(conf, "min-available") ?: 1
healthConfig.chains[chain] = HealthConfig.ChainConfig(chain, minAvailable.coerceAtLeast(0))
}
}
}

View File

@@ -25,4 +25,5 @@ class MainConfig {
var tokens: TokensConfig? = null
var monitoring: MonitoringConfig = MonitoringConfig.default()
var accessLogConfig: AccessLogConfig = AccessLogConfig.default()
var health: HealthConfig = HealthConfig.default()
}

View File

@@ -35,6 +35,7 @@ class MainConfigReader(
private val tokensConfigReader = TokensConfigReader()
private val monitoringConfigReader = MonitoringConfigReader()
private val accessLogReader = AccessLogReader()
private val healthConfigReader = HealthConfigReader()
fun read(input: InputStream): MainConfig? {
val configNode = readNode(input)
@@ -71,6 +72,9 @@ class MainConfigReader(
accessLogReader.read(input).let {
config.accessLogConfig = it
}
healthConfigReader.read(input).let {
config.health = it
}
return config
}
}

View File

@@ -0,0 +1,89 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.monitoring
import com.sun.net.httpserver.HttpServer
import io.emeraldpay.dshackle.config.HealthConfig
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import java.io.IOException
import java.net.InetSocketAddress
import javax.annotation.PostConstruct
@Service
class HealthCheckSetup(
@Autowired private val healthConfig: HealthConfig,
@Autowired private val multistreamHolder: MultistreamHolder
) {
companion object {
private val log = LoggerFactory.getLogger(HealthCheckSetup::class.java)
}
@PostConstruct
fun start() {
if (!healthConfig.isEnabled()) {
return
}
// use standard JVM server with a single thread blocking processing
// health check is a rare operation, no reason to set up anything complex
try {
log.info("Run Health Server on ${healthConfig.host}:${healthConfig.port}${healthConfig.path}")
val server = HttpServer.create(
InetSocketAddress(
healthConfig.host,
healthConfig.port
),
0
)
server.createContext(healthConfig.path) { httpExchange ->
val response = getHealth()
val ok = response == "OK"
val code = if (ok) HttpStatus.OK else HttpStatus.SERVICE_UNAVAILABLE
httpExchange.sendResponseHeaders(code.value(), response.toByteArray().size.toLong())
httpExchange.responseBody.use { os ->
os.write(response.toByteArray())
}
}
Thread(server::start).start()
} catch (e: IOException) {
log.error("Failed to start Health Server", e)
}
}
fun getHealth(): String {
val errors = healthConfig.configs().mapNotNull {
val up = multistreamHolder.getUpstream(it.blockchain)
if (up == null || !up.isAvailable()) {
return@mapNotNull "${it.blockchain} UNAVAILABLE"
}
val avail = up.getAll().count { it.getStatus() == UpstreamAvailability.OK }
if (avail < it.minAvailable) {
return@mapNotNull "${it.blockchain} LACKS MIN AVAILABILITY"
}
null
}
return if (errors.isEmpty()) {
"OK"
} else {
errors.joinToString("\n")
}
}
}