Usage guide

Installation

For sbt:

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

For ammonite:

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

Imports:

import sup._

Core concepts

Health

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

HealthResult

//simplified
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)
  • cats.data.NonEmptyList: there are multiple checks
  • sup.data.Report[cats.Id, cats.data.NonEmptyList, ?]: there’s one check, and a NonEmptyList of checks
  • sup.data.Report[sup.Tagged[String, ?], NonEmptyList, ?]: there’s one check, and a NonEmptyList of checks tagged with a String.

(sup.data.Report[F, G, ?] is equivalent to cats.data.OneAnd[Nested[F, G, ?], ?])

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

HealthCheck

//simplified
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.data._, 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.

HealthReporter

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

import sup.data.Report

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@33eb0b93

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

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

Tagging

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]sup.data.Tagged[String,H]] = sup.HealthCheck$$anon$1@15d74d87

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

val taggedReporter = HealthReporter.fromChecks(kafkaTagged, postgresTagged)
// taggedReporter: sup.HealthReporter[cats.effect.IO,cats.data.NonEmptyList,[H]sup.data.Tagged[String,H]] = sup.HealthCheck$$anon$1@6cc1809f

Modifiers

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:

timeoutToSick

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(scala.concurrent.ExecutionContext.global)
// contextShift: cats.effect.ContextShift[cats.effect.IO] = cats.effect.internals.IOContextShift@6dcabe00

implicit val timer: Timer[IO] = IO.timer(scala.concurrent.ExecutionContext.global)
// timer: cats.effect.Timer[cats.effect.IO] = cats.effect.internals.IOTimer@64967ab5

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

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]sup.data.Tagged[String,H]] = sup.HealthCheck$$anon$1@3d3ee172

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

recoverToSick

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@6b15d9eb

surround

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@674697fa