natchez
natchez copied to clipboard
Add Trace.spanR
Adds a way to span a resource, from acquisition through release.
See #514.
I am in favor of continuing with #527 over this one, particularly if https://github.com/typelevel/fs2/pull/2843 works out.
Is natchez okay to take on an fs2 dependency?
The repo already depends on it for xray. Not sure it's smart for core to take it on, but an fs2 module wouldn't hurt.
(Half-baked idea: as @zmccoy is fond of saying, tracing is most important around network calls. Having a natchez-fs2 that traced some of its io functions would be cool.)
It turns out a spanS is a lot easier than spanR for the Kleisli instance. translate gives us a hook to thread the state (here, just a String) through to the outputs:
def spanS[F[_], A](name: String)(s: Stream[Kleisli[F, String, *], A]): Stream[Kleisli[F, String, *], A] =
s.translate(
new (Kleisli[F, String, *] ~> Kleisli[F, String, *]) {
def apply[B](k: Kleisli[F, String, B]) = k.local(_ => name)
}
)
val s = Stream.eval(Kleisli.ask[IO, String])
(s ++ spanS("child")(s) ++ s)
.compile
.toList
.run("root")
.flatMap(IO.println)
prints List("root", "child", "root").
Tracing can be viewed as an exchange F ~> F of an untraced effect F with a traced effect F. This is witnessed by the polymorphic function that results from partially applying Trace.span.
If we redefine spanR as something that returns a resource of F ~> F, we decouple how we propagate the span from the structure of transformed effects. Thus, spanR supports a Trace[Resource[F, *], *], Trace[Stream[F, *], *], and any other "dynamically scoped" monad, all without introducing a second type class.
Expectation is that:
spanR(name).surround(fa) <-> span(name)(fa)
Caveats:
- I haven't tested any of these.
- The transformer instances still all require MonadCancelThrow.
Trace[Resource[F, *]]is still only ambient for acquisition and release, not use. That's the price of stateless instances.- Any
Streaminterruption hacks would have to be universal, because we're not defining a specificspanS.
The API for tracing a stream (example from https://github.com/armanbilge/bayou/issues/1) ends up looking like
def stream(implicit trace: Trace[IO]): Stream[IO, Unit] = {
Trace[Stream[IO, *]].span("new_root")(
Stream(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.covary[IO]
.chunkN(3)
.flatMap { chunk =>
Trace[Stream[IO, *]].span("child")(
Stream.eval(trace.put("vals" -> chunk.toList.mkString(","))) >>
Stream.sleep[IO](1.second)
)
}
)
}
To weave in and out of Stream, we have to accept the simpler Trace[IO] and then summon a Trace[Stream[IO, *]]. We could just reference trace if we made spanS a derived method on Trace, but then we'd need a MonadCancelThrow member, and fs2 would be forced into natchez-core.
Just figured I'd share that we've been using a custom Trace at work for the last few months that is essentially a combination of Bayou and the F ~> F implementation here and it's been working well for us.
I've open sourced it here. I don't intend to promote yet another tracing library. I'd much rather these concepts make it into Natchez.
Any thoughts on this design versus #527 now that some time has passed? @armanbilge @rossabaker
I moved onto otel4s and don't remember all the nuances, but I think the basic tradeoff was that this behaves similarly between stateless and stateful instances, and #527 was able to make the resource span the parent of use at the expense of stateless instances (and an extra type class.)
Thanks for picking it up.