Trace Constraint
We must be aware of the current Span
if we wish to add fields or create child spans, which means we must pass the current span around.
It is perfectly fine to pass the current span around explicitly, and if you are working in a concrete effect type like IO
in Cats-Effect 2 this is likely the best choice. However for the increasingly common case of applications constructed in tagless style (where computations are peformed in some abstract effect) we have a Trace
constraint that ensures an ambient span is always available.
The examples on this page use the following imports:
import cats.{ Applicative, Monad }
import cats.data.Kleisli
import cats.effect.MonadCancel
import cats.mtl.Local
import cats.syntax.all._
import natchez.{ Span, Trace }
import natchez.mtl._
Given an effect F[_]: Trace
we can add fields to the ambient span, gets its kernel, or run a computation in a child of the ambient span.
def wibble[F[_]: Monad: Trace](name: String, age: Int): F[Unit] =
Trace[F].span("wibble") {
for {
_ <- Trace[F].put("name" -> name, "age" -> age)
// ... actual method logic in F
} yield ()
}
By adding a Trace
constraint to our abstract we gain this capability.
Now that we have written a program in F[_]: Trace
we need a way to satisfy this constraint. There are several ways to do this.
No-Op Instance
There is a no-op Trace[F]
instance available for all F[_]: Applicative
. This instance does as you might expect: nothing. This can be useful for development and for testing (where trace data is typically not necessary).
object NoTraceForYou {
// In this scope we will ignore tracing
import natchez.Trace.Implicits.noop
// So this compiles fine
def go[F[_]: Monad]: F[Unit] =
wibble("bob", 42)
}
Kleisli Instance
Given MonadCancel[F, Throwable]
there is a Trace[F]
instance for Kleisli[F, Span[F], *]
. This allows us to discharge a Trace
constraint, given an initial Span
.
// Given MonadCancel[F, Throwable] and a Span[F] we can call `wibble`
def go[F[_]](span: Span[F])(
implicit ev: MonadCancel[F, Throwable]
): F[Unit] =
wibble[Kleisli[F, Span[F], *]]("bob", 42).run(span)
This strategy works if we write in tagless style. Our outer effect is instantiated as some effect F
and the effect for the traced part of our program is instantiated as Kleisli[F, Span[F], *]
.
It is not always possible instantiate arbitrary F
as Kleisli
, depending on the additional constraints on F
. In particular it would be impossbile to call wibble[F[_]: ConcurrentEffect: Trace]
in this way.
Cats-MTL Local Instance
By first adding the natchez-mtl
module (which pulls in Cats-MTL), given MonadCancel[F, Throwable]
and Local[F, Span[F]]
there is an instance Trace[F]
.
def goLocal[F[_]](
implicit ev0: MonadCancel[F, Throwable],
ev1: Local[F, Span[F]]
): F[Unit] =
wibble("bob", 42)
This is more general than the Kleisli
instance above and allows you to instantiate F
as RWST
for example, at the cost of an additional dependency.
Cats-Effect 3 IO Instance
Given a Span[F]
you can construct a Trace[IO]
for Cats-Effect 3 (for Cats-Effect 2 you will need to use Kleisli
or Local
above). This uses FiberLocal
to pass the span around.
TODO: Example