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.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

The source code for this page can be found here.