@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user