Usage guide


For sbt:

libraryDependencies += "com.kubukoz" %% "sup-core" % "0.6.0"

For ammonite:

import $ivy.`com.kubukoz::sup-core:0.6.0`


import sup._

Core concepts


Health is a boolean-like health status. In ADT notation:

data Health = Sick | Healthy

It has a commutative monoid, which is equivalent to the “all” monoid for booleans.

That means Health values can be combined with the following semantics:

|+| Sick Healthy
Sick Sick Sick
Healthy Sick Healthy


type HealthResult[H[_]] = H[Health]

HealthResult is a wrapper over a collection H of Health. There are no limitations about what kind of collection that must be, but it’s recommended that it has a cats.Foldable instance. For a single Health, cats.Id can be used.

Other examples of a suitable type include:

  • cats.Id: there’s only one result.
  • sup.Tagged[String, ?]: there’s only one result, tagged with a String (e.g. the dependency’s name)
  • there are multiple checks
  •[cats.Id,, ?]: there’s one check, and a NonEmptyList of checks
  •[sup.Tagged[String, ?], NonEmptyList, ?]: there’s one check, and a NonEmptyList of checks tagged with a String.

([F, G, ?] is equivalent to[Nested[F, G, ?], ?])

HealthResult[H] has a Monoid for any H[_]: Applicative, although most of its usages will be transparent to the user.


trait HealthCheck[F[_], H[_]] {
  def check: F[HealthResult[H]]

HealthCheck[F, H] is a health-checking action with effects of type F that’ll result in a collection H of Health.

Similarly to HealthResult, a HealthCheck[F, H] has a monoid for any F[_]: Applicative, H[_]: Applicative. This is really cool, because thanks to this we can combine two similar healhchecks into one that’ll check both (sequentially or in parallel, depending on the applicative of F - to enforce parallel execution use HealthCheck.parTupled).

Let’s start with some cats imports (assume they’re available in the rest of the page):

import cats._,, cats.effect._, cats.implicits._

and here’s how healthchecks can be combined:

//will always be Sick
def queue1: HealthCheck[IO, Id] = HealthCheck.const(Health.Sick)
// queue1: sup.HealthCheck[cats.effect.IO,cats.Id]

//will always be Healthy
def queue2: HealthCheck[IO, Id] = HealthCheck.const(Health.Healthy)
// queue2: sup.HealthCheck[cats.effect.IO,cats.Id]

//will always be Sick
def queues = queue1 |+| queue2
// queues: sup.HealthCheck[cats.effect.IO,cats.Id]

Advanced concepts

In sup, a single dependency’s healthcheck has the same underlying structure as a whole service’s healthcheck consisting of multiple dependencies’ checks.


A healthcheck wrapping multiple healthchecks is called a HealthReporter. Here’s how it’s defined in sup:


type HealthReporter[F[_], G[_], H[_]] = HealthCheck[F, Report[G, H, ?]]

You can construct one from a sequence of healthchecks using the HealthReporter.fromChecks function:

val kafka: HealthCheck[IO, Id] = HealthCheck.const(Health.Healthy)
// kafka: sup.HealthCheck[cats.effect.IO,cats.Id] = sup.HealthCheck$$anon$1@52c12d34

val postgres: HealthCheck[IO, Id] = HealthCheck.const(Health.Healthy)
// postgres: sup.HealthCheck[cats.effect.IO,cats.Id] = sup.HealthCheck$$anon$1@786c4b5e

val reporter: HealthReporter[IO, NonEmptyList, Id] = HealthReporter.fromChecks(kafka, postgres)
// reporter: sup.HealthReporter[cats.effect.IO,,cats.Id] = sup.HealthCheck$$anon$1@4a1ba497


A healthcheck can be tagged with a label, e.g. a String with the dependency’s name:

import sup.mods._
// import sup.mods._

val kafkaTagged = kafka.through(mods.tagWith("kafka"))
// kafkaTagged: sup.HealthCheck[cats.effect.IO,[H][String,H]] = sup.HealthCheck$$anon$1@3987964a

val postgresTagged = postgres.through(mods.tagWith("postgres"))
// postgresTagged: sup.HealthCheck[cats.effect.IO,[H][String,H]] = sup.HealthCheck$$anon$1@5322a164

val taggedReporter = HealthReporter.fromChecks(kafkaTagged, postgresTagged)
// taggedReporter: sup.HealthReporter[cats.effect.IO,,[H][String,H]] = sup.HealthCheck$$anon$1@690223c0


sup provides a variety of ways to customize a healthcheck. These include through, mapK, transform, mapResult and leftMapK. Check out the predefined modifiers in the sup.mods object or create your own.

Here are some example modifiers provided by sup:


A check modified with timeoutToSick will be marked as sick if it doesn’t complete within the given duration.

import scala.concurrent.duration._
// import scala.concurrent.duration._

implicit val contextShift: ContextShift[IO] = IO.contextShift(
// contextShift: cats.effect.ContextShift[cats.effect.IO] = cats.effect.internals.IOContextShift@5cc5e3b6

implicit val timer: Timer[IO] = IO.timer(
// timer: cats.effect.Timer[cats.effect.IO] = cats.effect.internals.IOTimer@74be7479

val timedKafka = kafka.through(mods.timeoutToSick(5.seconds))
// timedKafka: sup.HealthCheck[cats.effect.IO,cats.Id] = sup.HealthCheck$$anon$1@76830d12

Other modifiers with timeouts include timeoutToDefault and timeoutToFailure.

tagWith / untag

Tag a healthcheck with a value (or unwrap a tagged healthcheck):

val taggedKafka2 = kafka.through(mods.tagWith("foo"))
// taggedKafka2: sup.HealthCheck[cats.effect.IO,[H][String,H]] = sup.HealthCheck$$anon$1@2eb7cf01

val untaggedKafka = taggedKafka2.through(mods.untag)
// untaggedKafka: sup.HealthCheck[cats.effect.IO,cats.Id] = sup.HealthCheck$$anon$1@59c376da


Swallows any errors that might happen in the healthcheck’s effect and falls back to Sick.

val safeKafka = kafka.through(mods.recoverToSick)
// safeKafka: sup.HealthCheck[cats.effect.IO,cats.Id] = sup.HealthCheck$$anon$1@61ae2e8a


Surrounds a healthcheck with effectful actions.

val surroundedKafka = kafka.through(mods.surround(IO(println("foo")))(result => IO(println(s"foo result: $result"))))
// surroundedKafka: sup.HealthCheck[cats.effect.IO,cats.Id] = sup.HealthCheck$$anon$1@34821094