Playing Healthchecks

Healthchecks in the Scala Play Framework

The Play framework for Scala is “marketed” as a lightweight framework used all over for folks building microservices and other quick and nimble services. While it may be fast and opinionated, it definitely does not set you up for success in a microservice world.

Microservice Necessities

Microservices need a few things to survive. Pertinent to this topic, they need to be observable. This means that they need to expose certain pieces of information about themselves that would allow an operator to determine if an instance of the service is healthy or not. If an instance is not healthy, the operator should take action to either restore the instance to a healthy state, or replace the instance with a new (hopefully healthier) instance.

Kubernetes specifically (and you must be using Kubernetes to say you’re doing microservices, right?) breaks down these monitoring probes into two categories, a Readiness probe, and a Liveness probe. Yes, technically there is also the startup probe, but we are not concerned with that one at the moment. The Readiness probe tells Kubernetes (the operator) that the service is ready to do work, in other words it is ready to be enrolled in a load balancer. The Liveness probe tells Kubernetes that the service is alive.

If a service instance fails the Readiness probe, Kubernetes will remove it from the load balancer, which should prevent the instance from getting new work, allowing the instance to finish work it currently has and become healthy enough to take on new work again.

If a service instance fails the Liveness probe, Kubernetes will terminate it, and restart it.

Healthcheck

What does the Scala Play framework give you for adding healthchecks to your service? Nothing. Nothing at all. You can perhaps find some vague notes online on adding Dropwizard metrics libraries for getting metrics, and that happens to come with a healthcheck component as well, but that’s it. Nothing out of the box. Nothing even from a supported plugin or anything like that.

I opted to go the Dropwizard route, because that’s really the only thing I could find information for.

Dropwizard

Firstly, we need to add dependencies to our project. We need metrics-core and metrics-healthchecks:

"io.dropwizard.metrics" % "metrics-core"         % "4.2.23",
"io.dropwizard.metrics" % "metrics-healthchecks" % "4.2.23"

Then update the application configuration to enable the healthcheck. We use an APM tool, so I did not enable any JVM or other instrumentation that is available.

metrics {
  enabled = true
  jvm = false
  logback = false
  healthchecks {
    enabled = true
  }
}

In my case, the only dependency I really want to check in my healthcheck is our Database for now, so I then added a check for that

package infrastructure

import com.codahale.metrics.health.HealthCheck
import scala.concurrent.duration._
import scala.concurrent.{Await, ExecutionContext, Future}
import play.api.db.DBApi


class DatabaseHealthCheck (db: DBApi)(implicit val ec: ExecutionContext) extends HealthCheck {
  private val timeoutDuration = 5.seconds
  override def check(): HealthCheck.Result = {
    val database = db.database("default")
    val future: Future[Boolean] = Future {
      database.withConnection { conn =>
        conn.isValid(1)
      }
    }
    try {
      if (Await.result(future, timeoutDuration)) {
        return HealthCheck.Result.healthy()
      } else {
        return HealthCheck.Result.unhealthy("default db is not healthy")
      }
    } catch {
      case _: java.util.concurrent.TimeoutException =>
        return HealthCheck.Result.unhealthy("default db is not healthy, timeout while obtaining db connection")
    }
  }
}

This will check the “default” database to make sure it can connect to it, waiting up to 5 seconds for a connection to return and be valid. If no connection is returned in that time, or the connection that comes back is not valid for whatever reason, the healthcheck will respond as unhealthy.

Next I need to register the endpoint. Inside my REST controller I created a healthcheck registry (not the best practice, but I didn’t really have a better place to construct this and register the DB healthcheck), and then added an endpoint to respond with the healthcheck status.

import com.codahale.metrics.health.HealthCheckRegistry
import infrastructure.DatabaseHealthCheck
import scala.collection.JavaConverters._
import scala.concurrent.{ExecutionContext, Future}
import play.api.libs.json.Json
import play.api.db.DBApi

...

class RESTController @Inject()(db: DBApi)(implicit val ec: ExecutionContext) extends InjectedController {
  val healthCheckRegistry = new HealthCheckRegistry()
  healthCheckRegistry.register("database", new DatabaseHealthCheck(db))

  ...

  def healthcheck: Action[AnyContent] = Action.async { _ =>
    Future {
      val healthCheckResults = healthCheckRegistry.runHealthChecks()
      val resultsAsJson = healthCheckResults.asScala.map { case (name, result) =>
        Json.obj(
          "name" -> name,
          "isHealthy" -> result.isHealthy,
          "message" -> result.getMessage
        )
      }

      if (resultsAsJson.forall(_("isHealthy").as[Boolean])) {
        Ok(Json.obj("status" -> "healthy", "checks" -> resultsAsJson))
      } else {
        logger.warn("healthcheck results ${healthCheckResults.toString()}")
        ServiceUnavailable(Json.obj("status" -> "unhealthy", "checks" -> resultsAsJson))
      }
    }
  }

  ...
}

Expose that endpoint in routes:

GET         /healthcheck                                 controllers.RESTController.healthcheck

And now my service has a healthcheck that I can then use for my Kubernetes readiness probes!

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.