Skip to content

Commit

Permalink
Merge pull request #60 from ovotech/add-fallthrough-to
Browse files Browse the repository at this point in the history
Add fallthroughTo so that HttpRoutes and HttpApp can be composed
  • Loading branch information
tomverran authored May 11, 2021
2 parents bfd216b + 68cd69d commit c231233
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 4 deletions.
2 changes: 1 addition & 1 deletion docs/docs/docs/natchez-fs2.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ i.e. Kafka consumers using FS2.
val natchezExtrasVersion = "@VERSION@"

libraryDependencies ++= Seq(
"com.ovoenergy" %% "natchez-fs2" % natchezExtrasVersion
"com.ovoenergy" %% "natchez-extras-fs2" % natchezExtrasVersion
)
```

Expand Down
60 changes: 59 additions & 1 deletion docs/docs/docs/natchez-http4s.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ When it is merged this module will continue to exist but as a wrapper that adds
val natchezExtrasVersion = "@VERSION@"

libraryDependencies ++= Seq(
"com.ovoenergy" %% "natchez-fs2" % natchezExtrasVersion
"com.ovoenergy" %% "natchez-extras-http4s" % natchezExtrasVersion
)
```

Expand Down Expand Up @@ -108,6 +108,64 @@ Running the above app and hitting the `POST` endpoint should generate a trace li

![datadog trace]({{site.baseurl}}/img/example-http-trace.png)


## Tracing only some routes

Often you don't want to trace all of your routes, for example if you have a healthcheck route
that is polled by a load balancer every few seconds you may wish to exclude it from your traces.

You can do this using `.fallthroughTo` provided in the `syntax` package which allows the combination
of un-traced `HttpRoutes[F]` and the `HttpApp[F]` that the tracing middleware returns:

```scala mdoc
import cats.data.Kleisli
import cats.effect.{ExitCode, IO, IOApp, Resource}
import com.ovoenergy.natchez.extras.datadog.Datadog
import com.ovoenergy.natchez.extras.http4s.Configuration
import com.ovoenergy.natchez.extras.http4s.server.TraceMiddleware
import com.ovoenergy.natchez.extras.http4s.server.syntax.KleisliSyntax
import natchez.{EntryPoint, Span}
import org.http4s._
import org.http4s.client.blaze.BlazeClientBuilder
import org.http4s.dsl.io._
import org.http4s.server.blaze.BlazeServerBuilder
import org.http4s.syntax.kleisli._

import scala.concurrent.ExecutionContext.global

object Main extends IOApp {

type TraceIO[A] = Kleisli[IO, Span[IO], A]
val conf: Configuration[IO] = Configuration.default()

val datadog: Resource[IO, EntryPoint[IO]] =
for {
httpClient <- BlazeClientBuilder[IO](global).withDefaultSslContext.resource
entryPoint <- Datadog.entryPoint(httpClient, "example-http-api", "default-resource")
} yield entryPoint

val healthcheck: HttpRoutes[IO] =
HttpRoutes.of { case GET -> Root / "health" => Ok("healthy") }

val application: HttpRoutes[TraceIO] =
HttpRoutes.pure(Response(status = Status.InternalServerError))

def run(args: List[String]): IO[ExitCode] =
datadog.use { entryPoint =>

val combinedRoutes: HttpApp[IO] =
healthcheck.fallthroughTo(TraceMiddleware(entryPoint, conf)(application.orNotFound))

BlazeServerBuilder[IO](global)
.withHttpApp(combinedRoutes)
.bindHttp(port = 8080)
.serve
.compile
.lastOrError
}
}
```

## Configuration

Given that every HTTP API is likely to have different tracing requirements `natchez-http4s` attempts to be as configurable as possible.
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/docs/natchez-log4cats.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ the [agentless logging](https://docs.datadoghq.com/logs/log_collection/java/?tab
val natchezExtrasVersion = "@VERSION@"

libraryDependencies ++= Seq(
"com.ovoenergy" %% "natchez-fs2" % natchezExtrasVersion
"com.ovoenergy" %% "natchez-extras-log4cats" % natchezExtrasVersion
)
```

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/docs/natchez-testkit.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ that check your application is sending the right information to Natchez.
val natchezExtrasVersion = "@VERSION@"

libraryDependencies ++= Seq(
"com.ovoenergy" %% "natchez-testkit" % natchezExtrasVersion % Test
"com.ovoenergy" %% "natchez-extras-testkit" % natchezExtrasVersion % Test
)
```

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ovoenergy.natchez.extras.http4s.server

import cats.Monad
import cats.data.{Kleisli, OptionT}

object syntax {
/**
* Given an `A => F[Option[B]]` and an `A => F[B]` run the first function
* and if it returns F[None] then fall through to the second function.
* This is useful for composing HttpRoutes[F] with HttpApp[F]
*/
implicit class KleisliSyntax[F[_]: Monad, A, B](a: Kleisli[OptionT[F, *], A, B]) {
def fallthroughTo(b: Kleisli[F, A, B]): Kleisli[F, A, B] = Kleisli(r => a.run(r).getOrElseF(b.run(r)))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.ovoenergy.natchez.extras.http4s.server

import cats.effect.IO
import com.ovoenergy.natchez.extras.http4s.server.syntax._
import org.http4s.Status.{InsufficientStorage, Ok}
import org.http4s.{HttpApp, HttpRoutes, Request, Response}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec

class SyntaxTest extends AnyWordSpec with Matchers {

"fallThroughTo" should {

"Call the second Kleisli if the first returns None" in {
val routes: HttpRoutes[IO] = HttpRoutes.empty
val app: HttpApp[IO] = HttpApp.pure(Response(status = Ok))
val result = routes.fallthroughTo(app).run(Request()).unsafeRunSync()
result.status shouldBe Ok
}

"Not call the second Kleisli if the first returns a result" in {
val routes: HttpRoutes[IO] = HttpRoutes.pure(Response(status = Ok))
val app: HttpApp[IO] = HttpApp.pure(Response(status = InsufficientStorage))
val result = routes.fallthroughTo(app).run(Request()).unsafeRunSync()
result.status shouldBe Ok
}
}
}

0 comments on commit c231233

Please sign in to comment.