Skip to content

Commit

Permalink
Add startup checks
Browse files Browse the repository at this point in the history
  • Loading branch information
Philippus committed Aug 29, 2024
1 parent 990f663 commit dc05700
Show file tree
Hide file tree
Showing 15 changed files with 350 additions and 23 deletions.
6 changes: 3 additions & 3 deletions docs/src/main/paradox/healthchecks.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,14 @@ Application specific health checks can be added a `name = <fully qualified class
Health checks can be hosted via the Pekko management HTTP server. The `pekko.management.HealthCheckRoutes` is enabled
by default as a Pekko management route provider.

By default all readiness checks are hosted on `/ready` and liveness checks are hosted on `/alive`. If all of the checks
By default all startup checks are hosted on `/startup`, readiness checks are hosted on `/ready` and liveness checks are hosted on `/alive`. If all of the checks
for an endpoint succeed a `200` is returned, if any fail or return `false` a `500` is returned. The paths are
configurable via `pekko.management.health-checks.readiness-path` and `pekko.management.health-checks.liveness-path` e.g.
configurable via `pekko.management.health-checks.startup-path`, `pekko.management.health-checks.readiness-path` and `pekko.management.health-checks.liveness-path` e.g.

@@snip [application.conf](/integration-test/local/src/main/resources/application.conf) { #health }

The `org.apache.pekko.management.HealthCheckRoutes` can be disabled with the following configuration but that also
means that the configured `readiness-checks` and `liveness-checks` will not be used.
means that the configured `startup-checks`, `readiness-checks` and `liveness-checks` will not be used.

```
pekko.management.http.routes {
Expand Down
1 change: 1 addition & 0 deletions integration-test/local/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pekko.discovery {

#health
pekko.management.health-checks {
startup-path = "health/startup"
readiness-path = "health/ready"
liveness-path = "health/alive"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ class LocalBootstrapTest extends AnyWordSpec with ScalaFutures with Matchers wit
super.afterAll()
}

def startupStatusCode(port: Int)(implicit system: ActorSystem): StatusCode =
healthCheckStatus(port, "health/startup")
def readyStatusCode(port: Int)(implicit system: ActorSystem): StatusCode =
healthCheckStatus(port, "health/ready")
def aliveStatusCode(port: Int)(implicit system: ActorSystem): StatusCode =
Expand All @@ -112,6 +114,14 @@ class LocalBootstrapTest extends AnyWordSpec with ScalaFutures with Matchers wit
// for http client
implicit val system: ActorSystem = systems(0)

"not started up initially" in {
eventually {
managementPorts.foreach { port =>
startupStatusCode(port) shouldEqual StatusCodes.InternalServerError
}
}
}

"not be ready initially" in {
eventually {
managementPorts.foreach { port =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.

# Add startup checks
ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.management.scaladsl.HealthChecks.startup")
ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.management.scaladsl.HealthChecks.startupResult")
9 changes: 7 additions & 2 deletions management/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pekko.management {
# The FQCN is the fully qualified class name of the `ManagementRoutesProvider`.
#
# By default the `org.apache.pekko.management.HealthCheckRoutes` is enabled, see `health-checks` section of how
# configure specific readiness and liveness checks.
# configure specific startup, readiness and liveness checks.
#
# Route providers included by a library (from reference.conf) can be excluded by an application
# by using "" or null as the FQCN of the named entry, for example:
Expand All @@ -72,8 +72,10 @@ pekko.management {
route-providers-read-only = true
}

# Health checks for readiness and liveness
# Health checks for startup, readiness and liveness
health-checks {
# When exposing health checks via Pekko Management, the path to expose startup checks on
startup-path = "startup"
# When exposing health checks via Pekko Management, the path to expose readiness checks on
readiness-path = "ready"
# When exposing health checks via Pekko Management, the path to expose readiness checks on
Expand All @@ -90,6 +92,9 @@ pekko.management {
#
# Libraries and frameworks that contribute checks are expected to add their own checks to their reference.conf.
# Applications can add their own checks to application.conf.
startup-checks {

}
readiness-checks {

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ private[pekko] class HealthCheckRoutes(system: ExtendedActorSystem) extends Mana

override def routes(mrps: ManagementRouteProviderSettings): Route = {
concat(
path(PathMatchers.separateOnSlashes(settings.startupPath)) {
get {
onComplete(healthChecks.startupResult())(healthCheckResponse)
}
},
path(PathMatchers.separateOnSlashes(settings.readinessPath)) {
get {
onComplete(healthChecks.readyResult())(healthCheckResponse)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ object HealthCheckSettings {
}

new HealthCheckSettings(
config
.getConfig("startup-checks")
.root
.unwrapped
.asScala
.collect {
case (name, value) if validFQCN(value) => NamedHealthCheck(name, value.toString)
}
.toList,
config
.getConfig("readiness-checks")
.root
Expand All @@ -50,6 +59,7 @@ object HealthCheckSettings {
case (name, value) if validFQCN(value) => NamedHealthCheck(name, value.toString)
}
.toList,
config.getString("startup-path"),
config.getString("readiness-path"),
config.getString("liveness-path"),
config.getDuration("check-timeout").asScala)
Expand All @@ -63,34 +73,85 @@ object HealthCheckSettings {
/**
* Java API
*/
def create(
startupChecks: java.util.List[NamedHealthCheck],
readinessChecks: java.util.List[NamedHealthCheck],
livenessChecks: java.util.List[NamedHealthCheck],
startupPath: String,
readinessPath: String,
livenessPath: String,
checkDuration: java.time.Duration) =
new HealthCheckSettings(
startupChecks.asScala.toList,
readinessChecks.asScala.toList,
livenessChecks.asScala.toList,
startupPath,
readinessPath,
livenessPath,
checkDuration.asScala)

/**
* Java API
*/
@deprecated("Use create that takes `startupChecks` and `startupPath` parameters instead", "1.1.0")
def create(
readinessChecks: java.util.List[NamedHealthCheck],
livenessChecks: java.util.List[NamedHealthCheck],
readinessPath: String,
livenessPath: String,
checkDuration: java.time.Duration) =
new HealthCheckSettings(
Nil,
readinessChecks.asScala.toList,
livenessChecks.asScala.toList,
"",
readinessPath,
livenessPath,
checkDuration.asScala)
}

/**
* @param startupChecks List of FQCN of startup checks
* @param readinessChecks List of FQCN of readiness checks
* @param livenessChecks List of FQCN of liveness checks
* @param startupPath The path to serve startup on
* @param readinessPath The path to serve readiness on
* @param livenessPath The path to serve liveness on
* @param checkTimeout how long to wait for all health checks to complete
*/
final class HealthCheckSettings(
val startupChecks: immutable.Seq[NamedHealthCheck],
val readinessChecks: immutable.Seq[NamedHealthCheck],
val livenessChecks: immutable.Seq[NamedHealthCheck],
val startupPath: String,
val readinessPath: String,
val livenessPath: String,
val checkTimeout: FiniteDuration) {

@deprecated("Use constructor that takes `startupChecks` and `startupPath` parameters instead", "1.1.0")
def this(
readinessChecks: immutable.Seq[NamedHealthCheck],
livenessChecks: immutable.Seq[NamedHealthCheck],
readinessPath: String,
livenessPath: String,
checkTimeout: FiniteDuration
) = {
this(
Nil,
readinessChecks,
livenessChecks,
"",
readinessPath,
livenessPath,
checkTimeout
)
}

/**
* Java API
*/
def getStartupChecks(): java.util.List[NamedHealthCheck] = startupChecks.asJava

/**
* Java API
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ object ManagementLogMarker {
def boundHttp(boundAddress: String): LogMarker =
LogMarker("pekkoManagementBound", Map(Properties.HttpAddress -> boundAddress))

/**
* Marker "pekkoStartupCheckFailed" of log event when a startup check fails.
*/
val startupCheckFailed: LogMarker =
LogMarker("pekkoStartupCheckFailed")

/**
* Marker "pekkoReadinessCheckFailed" of log event when a readiness check fails.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import pekko.event.Logging
import pekko.management.{ HealthCheckSettings, InvalidHealthCheckException, ManagementLogMarker, NamedHealthCheck }
import pekko.management.javadsl.{ LivenessCheckSetup => JLivenessCheckSetup }
import pekko.management.javadsl.{ ReadinessCheckSetup => JReadinessCheckSetup }
import pekko.management.scaladsl.{ HealthChecks, LivenessCheckSetup, ReadinessCheckSetup }
import pekko.management.javadsl.{ StartupCheckSetup => JStartupCheckSetup }
import pekko.management.scaladsl.{ HealthChecks, LivenessCheckSetup, ReadinessCheckSetup, StartupCheckSetup }
import pekko.util.FutureConverters._
import pekko.util.ccompat.JavaConverters._

Expand All @@ -47,13 +48,29 @@ final private[pekko] class HealthChecksImpl(system: ExtendedActorSystem, setting

private val log = Logging.withMarker(system, classOf[HealthChecksImpl])

log.info(
"Loading startup checks [{}]",
settings.startupChecks.map(a => a.name -> a.fullyQualifiedClassName).mkString(", "))
log.info(
"Loading readiness checks [{}]",
settings.readinessChecks.map(a => a.name -> a.fullyQualifiedClassName).mkString(", "))
log.info(
"Loading liveness checks [{}]",
settings.livenessChecks.map(a => a.name -> a.fullyQualifiedClassName).mkString(", "))

private val startupChecks: immutable.Seq[HealthCheck] = {
val fromScaladslSetup = system.settings.setup.get[StartupCheckSetup] match {
case None => Nil
case Some(setup) => setup.createHealthChecks(system)
}
val fromJavadslSetup = system.settings.setup.get[JStartupCheckSetup] match {
case None => Nil
case Some(setup) => convertSuppliersToScala(setup.createHealthChecks(system))
}
val fromConfig = load(settings.startupChecks)
fromConfig ++ fromScaladslSetup ++ fromJavadslSetup
}

private val readiness: immutable.Seq[HealthCheck] = {
val fromScaladslSetup = system.settings.setup.get[ReadinessCheckSetup] match {
case None => Nil
Expand Down Expand Up @@ -138,6 +155,20 @@ final private[pekko] class HealthChecksImpl(system: ExtendedActorSystem, setting
}
}

def startupResult(): Future[Either[String, Unit]] = {
val result = check(startupChecks)
result.onComplete {
case Success(Right(())) =>
case Success(Left(reason)) =>
log.info(ManagementLogMarker.startupCheckFailed, reason)
case Failure(e) =>
log.warning(ManagementLogMarker.startupCheckFailed, e.getMessage)
}
result
}

def startup(): Future[Boolean] = startupResult().map(_.isRight)

def readyResult(): Future[Either[String, Unit]] = {
val result = check(readiness)
result.onComplete {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ final class HealthChecks(system: ExtendedActorSystem, settings: HealthCheckSetti

private val delegate = new HealthChecksImpl(system, settings)

/**
* Returns CompletionStage(result), containing the system's startup result
*/
def startupResult(): CompletionStage[CheckResult] =
delegate.startupResult().map(new CheckResult(_))(system.dispatcher).asJava

/**
* Returns CompletionStage(true) if the system has started
*/
def startup(): CompletionStage[java.lang.Boolean] =
startupResult().thenApply(((r: CheckResult) => r.isSuccess).asJava)

/**
* Returns CompletionStage(result), containing the system's readiness result
*/
Expand Down Expand Up @@ -64,6 +76,23 @@ final class HealthChecks(system: ExtendedActorSystem, settings: HealthCheckSetti
aliveResult().thenApply(((r: CheckResult) => r.isSuccess).asJava)
}

object StartupCheckSetup {

/**
* Programmatic definition of startup checks
*/
def create(createHealthChecks: JFunction[ActorSystem, JList[Supplier[CompletionStage[java.lang.Boolean]]]])
: StartupCheckSetup = {
new StartupCheckSetup(createHealthChecks)
}
}

/**
* Setup for startup checks, constructor is *Internal API*, use factories in [[StartupCheckSetup]]
*/
final class StartupCheckSetup private (
val createHealthChecks: JFunction[ActorSystem, JList[Supplier[CompletionStage[java.lang.Boolean]]]]) extends Setup

object ReadinessCheckSetup {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ object HealthChecks {
@DoNotInherit
abstract class HealthChecks {

/**
* Returns Future(true) if the system has started
*/
def startup(): Future[Boolean]

/**
* Returns Future(result) containing the system's startup result
*/
def startupResult(): Future[Either[String, Unit]]

/**
* Returns Future(true) if the system is ready to receive user traffic
*/
Expand All @@ -62,6 +72,23 @@ abstract class HealthChecks {
def aliveResult(): Future[Either[String, Unit]]
}

object StartupCheckSetup {

/**
* Programmatic definition of startup checks
*/
def apply(createHealthChecks: ActorSystem => immutable.Seq[HealthChecks.HealthCheck]): StartupCheckSetup = {
new StartupCheckSetup(createHealthChecks)
}

}

/**
* Setup for startup checks, constructor is *Internal API*, use factories in [[StartupCheckSetup]]
*/
final class StartupCheckSetup private (
val createHealthChecks: ActorSystem => immutable.Seq[HealthChecks.HealthCheck]) extends Setup

object ReadinessCheckSetup {

/**
Expand Down
Loading

0 comments on commit dc05700

Please sign in to comment.