Universal health-check with minimum dependencies

Build Status Maven Central License

More details can be found in this article.

Usage

libraryDependencies += "com.dbrsn" %% "universal-health-check-core" % "0.0.5"
libraryDependencies += "com.dbrsn" %% "universal-health-check-http4s" % "0.0.5"

Model

We will start with simple Status ADT with 2 possible data types: Ok and Failure.

@JsonCodec(encodeOnly = true)
sealed abstract class HealthCheckStatus(val isOk: Boolean) {
  def isFailure: Boolean = !isOk
}

object HealthCheckStatus {

  case object Ok extends HealthCheckStatus(isOk = true)

  final case class Failure(error: String) extends HealthCheckStatus(isOk = false)

}

We also need another one model class for abstracting of the check itself. Let's call this component HealthCheckElement

final case class HealthCheckElement[F[_]](
  name: String, 
  status: F[HealthCheckStatus], 
  metadata: Map[String, String]
)

We use type constructor F[_] here. We would like to keep the check as generic as possible. So, it will represent 2 possible checks:

  • The instructional check, which is not yet materialized and has to be evaluated to know the actual result of the check: HealthCheckElement[IO] (here I use IO monad from cats-effects).
  • Already materialized check with ready to use result: HealthCheckElement[Id] (here I use Id identity type from cats).

And the list of all possible checks I will hold in the following structure:

final case class HealthCheck[F[_]](
  statuses: NonEmptyVector[HealthCheckElement[F]]
) {

  def withCheck(name: String, check: F[HealthCheckStatus], metadata: Map[String, String] = Map.empty): HealthCheck[F] =
    HealthCheck(statuses.append(HealthCheckElement(name, check, metadata)))

}

Here we also use F[_] with possible values HealthCheck[IO] for checks-instructions and HealthCheck[Id] for already ready checks.

Health-check example

Here we can see some ready-to use helper methods for Postgres, Kafka or Akka health-checks. And that is how we are going to use them in the application code:

val config: Config = ConfigFactory.load()

val healthCheck: HealthCheck[IO] = HealthCheck
  .ok[IO]("App", (key: String) => Try(config.getString(key)), "metrics.tags")  // if we need to parse some `application.conf` data to metadata.
  .withActorSystemCheck(isActorSystemRunning, akka.actor.ActorSystem.Version, Some(akka.http.Version.current))  // We need to pre-fill isActorSystemRunning: Boolean flag. We also add versions of Akka Actor System and Akka.Http.
  .withPostgresCheck(db.run(sql"SELECT 1;".as[Int]))  // Health-check for Postgres will be just simple run "SELECT 1;". We use `slick` as a database driver here.
  .withKafkaProducerCheck(healthCheckProducer.send(_, _, _).map(m => m.hasOffset && m.hasTimestamp))  // Kafka Producer health-check is just sending heart-bit message to health-check topic
  .withCheck("CustomCheck", IO(isApplicationRunning).map(HealthCheckStatus(_, "Application is not running")))  // We can also add some custom check.

Http4s Health Check Server

All you need to do is just to start your server:

val healthCheckServer = HealthCheckServer[IO](8080, "0.0.0.0", () => healthCheck())
healthCheckServer.run()

Akka Http Health Check Server

Another option is that we can integrate healthcheck into our existed akka-http application.

val healthCheckRoute: Route = (get & path("healthcheck")) { ctx =>
  healthCheck().fold(v => complete(v), v => complete((ServiceUnavailable, v))).unsafeToFuture().flatMap(_(ctx))
}
// ...
val route: Route = handleExceptions(ApiExceptionHandler.handle)(concat(
  otherRoute,
  healthCheckRoute
))

Example of json output

In happy path our health-check can return following json:

{
  "statuses": [
    {
      "name": "App",
      "status": {
        "Ok": {}
      },
      "metadata": {}
    },
    {
      "name": "ActorSystem",
      "status": {
        "Ok": {}
      },
      "metadata": {
        "akka.actor.ActorSystem.Version": "2.5.11",
        "akka.http.Version.current": "10.1.1"
      }
    },
    {
      "name": "PostgresDatabase",
      "status": {
        "Ok": {}
      },
      "metadata": {}
    },
    {
      "name": "KafkaProducer",
      "status": {
        "Ok": {}
      },
      "metadata": {}
    }
  ]
}

In the case of failure, our health-check will return ServiceUnavailable status and will be the following:

{
  "statuses": [
    {
      "name": "App",
      "status": {
        "Ok": {}
      },
      "metadata": {}
    },
    {
      "name": "ActorSystem",
      "status": {
        "Ok": {}
      },
      "metadata": {
        "akka.actor.ActorSystem.Version": "2.5.11",
        "akka.http.Version.current": "10.1.1"
      }
    },
    {
      "name": "PostgresDatabase",
      "status": {
        "Failure": {
          "error": "db.default.db - Connection is not available, request timed out after 1004ms."
        }
      },
      "metadata": {}
    },
    {
      "name": "KafkaProducer",
      "status": {
        "Failure": {
          "error": "Expiring 1 record(s) for health-check-0: 2034 ms has passed since batch creation plus linger time"
        }
      },
      "metadata": {}
    }
  ]
}