Usage guide
Installation
For sbt:
libraryDependencies += "com.kubukoz" %% "sup-core" % "0.7.0"
For ammonite:
import $ivy.`com.kubukoz::sup-core:0.7.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 checkssup.data.Report[cats.Id, cats.data.NonEmptyList, ?]
: there’s one check, and aNonEmptyList
of checkssup.data.Report[sup.Tagged[String, ?], NonEmptyList, ?]
: there’s one check, and aNonEmptyList
of checks tagged with aString
.
(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._
implicit val contextShift: ContextShift[IO] = IO.contextShift(scala.concurrent.ExecutionContext.global)
implicit val timer: Timer[IO] = IO.timer(scala.concurrent.ExecutionContext.global)
and here’s how healthchecks can be combined:
//will always be Sick
def queue1: HealthCheck[IO, Id] = HealthCheck.const(Health.Sick)
//will always be Healthy
def queue2: HealthCheck[IO, Id] = HealthCheck.const(Health.Healthy)
//will always be Sick
def queues = queue1 |+| queue2
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:
import sup.data.HealthReporter //Import actual HealthReporter //Import actual HealthReporter
val kafka: HealthCheck[IO, Id] = HealthCheck.const(Health.Healthy)
// kafka: HealthCheck[IO, Id] = sup.HealthCheck$$anon$1@3e7c8749
val postgres: HealthCheck[IO, Id] = HealthCheck.const(Health.Healthy)
// postgres: HealthCheck[IO, Id] = sup.HealthCheck$$anon$1@1fa7df6e
val reporter: HealthReporter[IO, NonEmptyList, Id] = HealthReporter.fromChecks(kafka, postgres)
// reporter: HealthReporter[IO, NonEmptyList, Id] = sup.HealthCheck$$anon$1@566f78a7
Tagging
A healthcheck can be tagged with a label, e.g. a String
with the dependency’s name:
val kafkaTagged = kafka.through(mods.tagWith("kafka"))
// kafkaTagged: HealthCheck[IO, sup.data.Tagged[String, H]] = sup.HealthCheck$$anon$1@77c26012
val postgresTagged = postgres.through(mods.tagWith("postgres"))
// postgresTagged: HealthCheck[IO, sup.data.Tagged[String, H]] = sup.HealthCheck$$anon$1@5b151188
val taggedReporter = HealthReporter.fromChecks(kafkaTagged, postgresTagged)
// taggedReporter: HealthReporter[IO, NonEmptyList, sup.data.Tagged[String, H]] = sup.HealthCheck$$anon$1@75055e66
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._
val timedKafka = kafka.through(mods.timeoutToSick(5.seconds))
// timedKafka: HealthCheck[IO, Id] = sup.HealthCheck$$anon$1@7be7a3f7
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: HealthCheck[IO, sup.data.Tagged[String, H]] = sup.HealthCheck$$anon$1@4e571daa
val untaggedKafka = taggedKafka2.through(mods.untag)
// untaggedKafka: HealthCheck[IO, Id] = sup.HealthCheck$$anon$1@563a4b29
recoverToSick
Swallows any errors that might happen in the healthcheck’s effect and falls back to Sick
.
val safeKafka = kafka.through(mods.recoverToSick)
// safeKafka: HealthCheck[IO, Id] = sup.HealthCheck$$anon$1@2435960
surround
Surrounds a healthcheck with effectful actions.
val surroundedKafka = kafka.through(mods.surround(IO(println("foo")))(result => IO(println(s"foo result: $result"))))
// surroundedKafka: HealthCheck[IO, Id] = sup.HealthCheck$$anon$1@4517ed44