diff --git a/build.sbt b/build.sbt index 5e5e217..250bbb3 100644 --- a/build.sbt +++ b/build.sbt @@ -108,4 +108,35 @@ lazy val examples = project "org.http4s" %% "http4s-ember-server" % "0.21.15", "org.slf4j" % "slf4j-simple" % "1.7.30", ).filterNot(_ => isDotty.value) - ) \ No newline at end of file + ) + +lazy val docs = project + .in(file("modules/docs")) + .dependsOn(http4s) + .enablePlugins(AutomateHeaderPlugin) + .enablePlugins(ParadoxPlugin) + .enablePlugins(ParadoxSitePlugin) + .enablePlugins(GhpagesPlugin) + .enablePlugins(MdocPlugin) + .settings(commonSettings) + .settings( + scalacOptions := Nil, + git.remoteRepo := "git@github.com:tpolecat/natchez.git", + ghpagesNoJekyll := true, + publish / skip := true, + paradoxTheme := Some(builtinParadoxTheme("generic")), + version := version.value.takeWhile(_ != '+'), // strip off the +3-f22dca22+20191110-1520-SNAPSHOT business + paradoxProperties ++= Map( + "scala-versions" -> (crossScalaVersions in http4s).value.map(CrossVersion.partialVersion).flatten.distinct.map { case (a, b) => s"$a.$b"} .mkString("/"), + "org" -> organization.value, + "scala.binary.version" -> s"2.${CrossVersion.partialVersion(scalaVersion.value).get._2}", + "core-dep" -> s"${(http4s / name).value}_2.${CrossVersion.partialVersion(scalaVersion.value).get._2}", + "version" -> version.value, + "scaladoc.natchez.base_url" -> s"https://static.javadoc.io/org.tpolecat/natchez-core_2.13/${version.value}", + ), + mdocIn := (baseDirectory.value) / "src" / "main" / "paradox", + Compile / paradox / sourceDirectory := mdocOut.value, + makeSite := makeSite.dependsOn(mdoc.toTask("")).value, + mdocExtraArguments := Seq("--no-link-hygiene"), // paradox handles this + ) + diff --git a/modules/docs/src/main/paradox/_template/js/link_fix.js b/modules/docs/src/main/paradox/_template/js/link_fix.js new file mode 100644 index 0000000..6d9604b --- /dev/null +++ b/modules/docs/src/main/paradox/_template/js/link_fix.js @@ -0,0 +1,3 @@ +function sourceUrlFix(sourceUrl) { + $("#source-link").attr("href", sourceUrl.replace("target/mdoc", "src/main/paradox")) +} diff --git a/modules/docs/src/main/paradox/_template/source.st b/modules/docs/src/main/paradox/_template/source.st new file mode 100644 index 0000000..69b44f2 --- /dev/null +++ b/modules/docs/src/main/paradox/_template/source.st @@ -0,0 +1,9 @@ + + +$if(page.source_url)$ +
+The source code for this page can be found here. +
+$endif$ + + diff --git a/modules/docs/src/main/paradox/index.md b/modules/docs/src/main/paradox/index.md new file mode 100644 index 0000000..17808f6 --- /dev/null +++ b/modules/docs/src/main/paradox/index.md @@ -0,0 +1,92 @@ +# Natchez-Http4s + +This is a support library for using [Natchez]() with [Http4s](). It provides the following things: + +1. A mechanism to discharge a `Trace[F]` constraint on `HttpRoutes[F]`, which constructs the required ambient span from incoming headers when possible, otherwise creating a root span. +1. A middleware which adds standard request/response header information to the top-level span associated with a request, as well as extended fields for failure cases. + +See below, then check out the `examples/` module in the repo for a working example with a real tracing back-end. + +## Tracing HttpRoutes + +Here is the basic pattern. + +- Construct `HttpRoutes` in abstract `F` with a `Trace` constraint. +- Lift it into our `EntryPoint`'s `F`, which has _no_ `Trace` constraint. + +```scala mdoc +// Nothing new here +import cats.effect.Bracket +import natchez.{ EntryPoint, Trace } +import org.http4s.HttpRoutes + +// This import provides `liftT`, used below. +import natchez.http4s.implicits._ + +// Our routes constructor is parametric in its effect, which has (at least) a +// `Trace` constraint. This means all our handlers will be in the same F and +// will have tracing available. +def mkTracedRoutes[F[_]: Trace]: HttpRoutes[F] = + ??? + +// Given an EntryPoint in F, with (at least) `Bracket[F, Throwable]` but +// WITHOUT a Trace constraint, we can lift `mkTracedRoutes` into F. +def mkRoutes[F[_]](ep: EntryPoint[F])( + implicit ev: Bracket[F, Throwable] +): HttpRoutes[F] = + ep.liftT(mkTracedRoutes) +``` + +The trick here is that `liftT` takes an `HttpRoutes[Kleisl[F, Span[F], *]]`, but this type is usually inferred so we never know or care that we're using `Kleisli`. If you do need to provide an explicit type argument, that's what it is. + +## Tracing HttpRoutes Resources + +We also provide `liftR`, which is analogous to `liftT` but works for `Resource[F, HttpRoutes[F]]`. + +```scala mdoc +import cats.Defer +import cats.effect.Resource + +def mkTracedRoutesResource[F[_]: Trace]: Resource[F, HttpRoutes[F]] = + ??? + +// Note that liftR also requires Defer[F] +def mkRoutesResource[F[_]: Defer](ep: EntryPoint[F])( + implicit ev: Bracket[F, Throwable] +): Resource[F, HttpRoutes[F]] = + ep.liftR(mkTracedRoutesResource) +``` + +## Middleware + +`NatchezMiddleware` adds the following "standard" fields to the top-level span associated with each request. + +| Field | Type | Description | +|--------------------|------------|---------------------------------------| +| `http.method` | String | `"GET"`, `"PUT"`, etc. | +| `http.url` | String | request URI | +| `http.status_code` | String (!) | `"200"`, `"403"`, etc. | +| `error` | Boolean | `true`, only present in case of error | + +In addition, the following Natchez-only fields are added. + +| Field | Type | Description | +|--------------------|--------|----------------------------------------------| +| `error.message` | String | Exception message | +| `error.stacktrace` | String | Exception stack trace as a multi-line string | + +Usage is straightforward. + +```scala mdoc +import natchez.http4s.NatchezMiddleware + +def mkRoutes2[F[_]](ep: EntryPoint[F])( + implicit ev: Bracket[F, Throwable] +): HttpRoutes[F] = + ep.liftT(NatchezMiddleware(mkTracedRoutes)) // type arguments are inferred as above +``` + +## Limitations + +Because we're instantiating `F` to `Kleisl[F, Span[F], *]` in our `HttpRoutes`, any constraints we place on `F` must be fulfillable by `Kleisli`. Things like `Monad` and `Deferred` and so on are fine, but effects that require the ability to extract a value (specifically `Effect` and `ConcurrentEffect`) are unavailable and will lead to a type error. So be aware of this constraint. + diff --git a/modules/examples/src/main/scala-2/Example1.scala b/modules/examples/src/main/scala-2/Example1.scala index 2ca9dfc..07f7fd6 100644 --- a/modules/examples/src/main/scala-2/Example1.scala +++ b/modules/examples/src/main/scala-2/Example1.scala @@ -19,6 +19,23 @@ import org.http4s.server.Server import org.http4s.implicits._ import natchez.http4s.NatchezMiddleware +/** + * Start up Jaeger thus: + * + * docker run -d --name jaeger \ + * -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \ + * -p 5775:5775/udp \ + * -p 6831:6831/udp \ + * -p 6832:6832/udp \ + * -p 5778:5778 \ + * -p 16686:16686 \ + * -p 14268:14268 \ + * -p 9411:9411 \ + * jaegertracing/all-in-one:1.8 + * + * Run this example and do some requests. Go to http://localhost:16686 and select `Http4sExample` + * and search for traces. +*/ object Http4sExample extends IOApp { // A dumb subroutine that does some tracing diff --git a/project/plugins.sbt b/project/plugins.sbt index 69ef488..a2434da 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,3 +6,4 @@ addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.3") addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.16") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.5.1") addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.2") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.2.16") \ No newline at end of file