Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add startup checks #293

Merged
merged 1 commit into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this class is already final, so no annotation necessary here 👍

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 {
Comment on lines 39 to 40
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this one already has DoNotInherit, so indeed the ReversedMissingMethodProblem MiMa warnings can be safely ignored here 👍


/**
* 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
Loading