-
Notifications
You must be signed in to change notification settings - Fork 78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
http4s module #5
Comments
Hey! I'd definitely like to do this but I'm not quite sure what the end-user API is going to look like yet so things may be changing underneath you. If you're ok with that I would welcome an http4s module. I'm curious, which back end are you going to use? I have been focusing on Honeycomb. |
@tpolecat I totally understand. To be honest, I had started something very similar to this project internally at my company but didn't have the time to fully flesh it out. I was essentially going to port the http4s stuff I had created over to this. It's not a lot of code so any changes shouldn't be too rough :) I'd most likely be using Jaeger as my backend, which I see already has a module! |
Ok I'll see if I can get a handle on the API this week. I'll open PRs for everything so we can discuss changes. Thanks for your interest! |
I'm very interested in this, mainly from the PoV that when using the Klieisli tracer there is no derived instance for Do either of you have a different method for injecting a |
I'm going to answer my own question here in case anyone is wondering about the same thing: When constructing routes or a http app where The natural transformations will have to be constructed inside the Using a bastardised version of the example module, this is what this method would look like: import org.http4s.syntax.all
...
val fk = new (F ~> Kleisli[F, Span[F], ?]) {
def apply(fa: F[A]): Kleisli[F, Span[F], ?] = Kleisli.liftF(fa)
}
entryPoint[F].use { ep =>
ep.root("root").use { span =>
val gk = new (Kleisli[F, Span[F], ?] ~> F) {
def apply(fa: Kleisli[F, Span[F], ?]): F[A] = fa.run(span)
}
val routes: HttpRoutes[Kleisli[F, Span[F], ?]] = ???
val transformedRoutes: HttpRoutes[F] = routes.transform(gk)(fk)
}
} Once the API is stabilised I could create a proper example of this if people are interested. |
Hi @janstenpickle. I've implemented a middleware that creates a new span per request: Exampleimport cats.data.{Kleisli, OptionT}
import cats.effect._
import cats.implicits._
import cats.~>
import io.jaegertracing.Configuration.{ReporterConfiguration, SamplerConfiguration}
import natchez.jaeger.Jaeger
import natchez.{EntryPoint, Span, TraceValue}
import org.http4s._
import org.http4s.dsl.Http4sDsl
import org.http4s.syntax.all._
object NatchezHttp4sExample extends IOApp {
type Traced[F[_], A] = Kleisli[F, Span[F], A]
class Api[F[_]: Sync] extends Http4sDsl[F] {
def routes: Http[OptionT[Traced[F, ?], ?], F] = tracedRoutes {
case GET -> Root / path =>
Kleisli { span =>
span.span("child-span").use { s =>
s.put("path" -> TraceValue.StringValue(path)) >> Ok("It works")
}
}
}
def tracedRoutes(pf: PartialFunction[Request[F], Traced[F, Response[F]]]): Http[OptionT[Traced[F, ?], ?], F] =
Kleisli { req =>
OptionT(Sync[Traced[F, ?]].suspend(pf.lift(req).sequence))
}
}
def withApiSpan[F[_]: Sync](ep: EntryPoint[F]): OptionT[Traced[F, ?], ?] ~> OptionT[F, ?] = {
val gk: Traced[F, ?] ~> F = new (Traced[F, ?] ~> F) {
def apply[A](fa: Kleisli[F, Span[F], A]): F[A] =
ep.root("api").use(fa.run)
}
new (OptionT[Traced[F, ?], ?] ~> OptionT[F, ?]) {
override def apply[A](fa: OptionT[Traced[F, ?], A]): OptionT[F, A] =
fa.mapK(gk)
}
}
override def run(args: List[String]): IO[ExitCode] =
entryPoint[IO].use { ep =>
val api = new Api[IO]
val routes = api.routes.mapK(withApiSpan[IO](ep)).orNotFound
val requests = List(
Request[IO](Method.GET, uri"/path1"),
Request[IO](Method.GET, uri"/path2"),
Request[IO](Method.GET, uri"/path3")
)
requests.traverse(routes.run).as(ExitCode.Success)
//BlazeServerBuilder[IO].withHttpApp(routes).resource.use(_ => IO.never) // Real use case
}
def entryPoint[F[_]: Sync]: Resource[F, EntryPoint[F]] =
Jaeger.entryPoint[F]("http4s-example") { c =>
Sync[F].delay {
c.withSampler(SamplerConfiguration.fromEnv)
.withReporter(ReporterConfiguration.fromEnv)
.getTracer
}
}
} Personally, I don't like this approach for several reasons:
In another project, I've been using a different solution based on ApplicativeHandle from Cats MTL. I've described span as an ADT and it was a part of the effect. Exampleimport cats.effect._
import cats.implicits._
import cats.mtl._
import cats.mtl.implicits._
import io.{opentracing => ot}
import natchez.{Kernel, TraceValue}
sealed trait Span
object Span {
final case object Empty extends Span
final case class Defined(tracer: ot.Tracer, otSpan: ot.Span) extends Span {
def kernel[F[_]: Sync]: F[Kernel] = ???
def put[F[_]: Sync](fields: (String, TraceValue)*): F[Unit] = ???
def span[F[_]: Sync](name: String): Resource[F, Defined] = ???
}
}
trait EntryPoint[F[_]] {
def root(name: String): Resource[F, Span.Defined]
def continue(name: String, kernel: Kernel): Resource[F, Span.Defined]
}
trait Tracer[F[_]] {
def current: F[Span]
def rootSpan[A](name: String)(fa: F[A]): F[A]
def childSpan[A](name: String)(fa: F[A]): F[A]
def put(fields: (String, TraceValue)*): F[Unit]
}
object Tracer {
def apply[F[_]](implicit ev: Tracer[F]): Tracer[F] = ev
def create[F[_]: Sync: ApplicativeLocal[?[_], Span]](ep: EntryPoint[F]): Tracer[F] =
new Tracer[F] {
override def current: F[Span] = ApplicativeLocal[F, Span].ask
override def rootSpan[A](name: String)(fa: F[A]): F[A] =
ep.root(name).use(context => fa.scope(context: Span))
override def childSpan[A](name: String)(fa: F[A]): F[A] =
current.flatMap {
case Span.Empty => fa
case s: Span.Defined => s.span(name).use(context => fa.scope(context: Span))
}
override def put(fields: (String, TraceValue)*): F[Unit] =
current.flatMap {
case Span.Empty => Sync[F].unit
case s: Span.Defined => s.put[F](fields: _*)
}
}
}
class Service[F[_]: Sync: ApplicativeLocal[?[_], Span]: Tracer] {
def foo(): F[Unit] =
Tracer[F].rootSpan("rootSpan") {
Tracer[F].put("key" -> "value") >> Sync[F].delay(println("It works"))
}
}
object NatchezCatsMtl extends IOApp {
override def run(args: List[String]): IO[ExitCode] = {
type Effect[A] = Kleisli[IO, Span, A]
entryPoint[Effect]
.use { ep =>
implicit val tracer: Tracer[Effect] = Tracer.create[Effect](ep)
val service = new Service[Kleisli[IO, Span, ?]]
service.foo()
}
.run(Span.Empty)
.as(ExitCode.Success)
}
def entryPoint[F[_]]: Resource[F, EntryPoint[F]] = ???
}
This one much more flexible and can be easily integrated with Http4s. WDYT @tpolecat ? |
Thanks for your comments, I will try to have a look soon but I'm super busy today. |
@janstenpickle thanks! Trace as Kleisliobject Trace {
def apply[F[_]](implicit ev: Trace[F]): Trace[F] = ev
def fromKleisli[F[_]: Sync](ep: EntryPoint[F]): Trace[Kleisli[F, Span, ?]] =
new Trace[Kleisli[F, Span, ?]] {
type Eff[A] = Kleisli[F, Span, A]
override def current: Eff[Span] = Kleisli.ask
override def rootSpan[A](name: String)(fa: Eff[A]): Eff[A] =
ep.root(name).mapK(Kleisli.liftK[F, Span]).use[A](context => Kleisli.local({_: Span => context: Span})(fa))
override def childSpan[A](name: String)(fa: Eff[A]): Eff[A] =
current.flatMap {
case Span.Empty =>
fa
case s: Span.Defined =>
s.span(name).mapK(Kleisli.liftK[F, Span]).use[A](context => Kleisli.local({_: Span => context: Span})(fa))
}
override def put(fields: (String, TraceValue)*): Eff[Unit] =
current.flatMap {
case Span.Empty => Sync[Eff].unit
case s: Span.Defined => s.put[Eff](fields: _*)
}
}
}
object NatchezCatsMtl extends IOApp {
override def run(args: List[String]): IO[ExitCode] = {
type Effect[A] = Kleisli[IO, Span, A]
entryPoint[IO]
.use { ep =>
implicit val trace: Trace[Effect] = Trace.fromKleisli[IO](ep)
val service = new Service[Kleisli[IO, Span, ?]]
service.foo().run(Span.Empty)
}
.as(ExitCode.Success)
}
def entryPoint[F[_]]: Resource[F, EntryPoint[F]] = ???
} This implementation has two differences from the natchez library:
The downside of ADT is that you cannot guarantee at the compile time that root span is already created. For example, |
Here is what I came up with. This lets you write your routes with a import cats.effect._
import cats.implicits._
import io.jaegertracing.Configuration._
import natchez._
import natchez.http4s.implicits._
import natchez.jaeger.Jaeger
import org.http4s._
import org.http4s.dsl.Http4sDsl
import org.http4s.implicits._
import org.http4s.server._
import org.http4s.server.blaze.BlazeServerBuilder
object Http4sExample extends IOApp {
// This is what we want to write: routes in F[_]: ...: Trace
def routes[F[_]: Sync: Trace]: HttpRoutes[F] = {
object dsl extends Http4sDsl[F]; import dsl._
HttpRoutes.of[F] {
case GET -> Root / "hello" / name =>
Trace[F].put("woot" -> 42) *>
Trace[F].span("responding") {
Ok(s"Hello, $name.")
}
}
}
// Normal constructor for an HttpApp in F *without* a Trace constraint.
def app[F[_]: Sync: Bracket[?[_], Throwable]](ep: EntryPoint[F]): HttpApp[F] =
Router("/" -> ep.liftT(routes)).orNotFound // <-- Lifted routes
// Normal server resource
def server[F[_]: ConcurrentEffect: Timer](routes: HttpApp[F]): Resource[F, Server[F]] =
BlazeServerBuilder[F]
.bindHttp(8080, "localhost")
.withHttpApp(routes)
.resource
// Normal EntryPoint resource
def entryPoint[F[_]: Sync]: Resource[F, EntryPoint[F]] =
Jaeger.entryPoint[F]("natchez-example") { c =>
Sync[F].delay {
c.withSampler(SamplerConfiguration.fromEnv)
.withReporter(ReporterConfiguration.fromEnv)
.getTracer
}
}
// Main method instantiates F to IO
def run(args: List[String]): IO[ExitCode] =
entryPoint[IO].map(app(_)).flatMap(server(_)).use(_ => IO.never).as(ExitCode.Success)
} The implementation is a little janky, may be able to simplify. But it works. package natchez.http4s
import cats.~>
import cats.data.{ Kleisli, OptionT }
import cats.effect.Bracket
import natchez.{ EntryPoint, Kernel, Span }
import org.http4s.HttpRoutes
object implicits {
// Given an entry point and HTTP Routes in Kleisli[F, Span[F], ?] return routes in F. A new span
// is created with the URI path as the name, either as a continuation of the incoming trace, if
// any, or as a new root. This can likely be simplified, I just did what the types were saying
// and it works so :shrug:
private def liftT[F[_]: Bracket[?[_], Throwable]](
entryPoint: EntryPoint[F])(
routes: HttpRoutes[Kleisli[F, Span[F], ?]]
): HttpRoutes[F] =
Kleisli { req =>
type G[A] = Kleisli[F, Span[F], A]
val lift = λ[F ~> G](fa => Kleisli(_ => fa))
val kernel = Kernel(req.headers.toList.map(h => (h.name.value -> h.value)).toMap)
val spanR = entryPoint.continueOrElseRoot(req.uri.path, kernel)
OptionT {
spanR.use { span =>
val lower = λ[G ~> F](_(span))
routes.run(req.mapK(lift)).mapK(lower).map(_.mapK(lower)).value
}
}
}
implicit class EntryPointOps[F[_]](self: EntryPoint[F]) {
def liftT(routes: HttpRoutes[Kleisli[F, Span[F], ?]])(
implicit ev: Bracket[F, Throwable]
): HttpRoutes[F] =
implicits.liftT(self)(routes)
}
} Hitting the endpoint yields a trace like I think this provides exactly what I want. WDYT? |
@tpolecat LGTM. One little thing. I would like to have a bit more control over headers passed to Kernel: The Logger middleware from Http4s provides a way to redact headers: def httpRoutes[F[_]: Concurrent](
logHeaders: Boolean,
logBody: Boolean,
redactHeadersWhen: CaseInsensitiveString => Boolean = Headers.SensitiveHeaders.contains,
logAction: Option[String => F[Unit]] = None
)(httpRoutes: HttpRoutes[F]): HttpRoutes[F] = Is this applicable here? |
@tpolecat how would you handle situation when |
You're out of luck in that case, although you could pass the span explicitly.
|
Blaze client is my case actually :) other place that I found static files/webjar service in Htt4s. I will try to pass Span explicitly for the time being. |
Ended up with this: def kleisliConcurrentEffect[F[_]](
root: Span[F]
)(implicit F: Concurrent[Kleisli[F, Span[F], *]], FF: ConcurrentEffect[F]): ConcurrentEffect[Kleisli[F, Span[F], *]] =
new ConcurrentEffect[Kleisli[F, Span[F], *]] {
override def runCancelable[A](fa: Kleisli[F, Span[F], A])(cb: Either[Throwable, A] => IO[Unit]): SyncIO[Kleisli[F, Span[F], Unit]] =
FF.runCancelable(fa.run(root))(cb).map(Kleisli.liftF)
override def runAsync[A](fa: Kleisli[F, Span[F], A])(cb: Either[Throwable, A] => IO[Unit]): SyncIO[Unit] =
FF.runAsync(fa.run(root))(cb)
// All other implementation from Concurrent[Kleisli[F, Span[F], *]]
}
implicit val a = kleisliConcurrentEffect[IO](NoopSpan[IO]()) // since this is for a dependency that doesn't need tracing |
Hey @tpolecat would you be open to a PR adding an http4s module for this project? Wrapping a
Client
, middleware for a server, maybe other things? If so, I'll try to get something created soonishThe text was updated successfully, but these errors were encountered: