Skip to content

Commit eb18ee6

Browse files
authored
Add an HTTP4S client integration (#52)
* Add an HTTP4s client integration * Add newline * Update docs
1 parent c59a435 commit eb18ee6

File tree

9 files changed

+149
-16
lines changed

9 files changed

+149
-16
lines changed

build.sbt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,14 @@ lazy val natchezSlf4j = project
7171

7272
lazy val natchezHttp4s = project
7373
.in(file("natchez-http4s"))
74+
.dependsOn(natchezTestkit)
7475
.enablePlugins(GitVersioning)
7576
.settings(common :+ (name := "natchez-http4s"))
7677
.settings(
7778
libraryDependencies ++= Seq(
78-
"org.tpolecat" %% "natchez-core" % natchezVersion,
79-
"org.http4s" %% "http4s-dsl" % http4sVersion
79+
"org.tpolecat" %% "natchez-core" % natchezVersion,
80+
"org.http4s" %% "http4s-client" % http4sVersion,
81+
"org.http4s" %% "http4s-dsl" % http4sVersion
8082
)
8183
)
8284

docs/docs/docs/natchez-http4s.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ import cats.effect.{ExitCode, IO, IOApp, Resource, Sync, Timer}
3131
import cats.syntax.flatMap._
3232
import cats.syntax.functor._
3333
import com.ovoenergy.effect.natchez.Datadog
34-
import com.ovoenergy.effect.natchez.http4s.server.{Configuration, TraceMiddleware}
34+
import com.ovoenergy.effect.natchez.http4s.Configuration
35+
import com.ovoenergy.effect.natchez.http4s.server.TraceMiddleware
3536
import natchez.{EntryPoint, Span, Trace}
3637
import org.http4s.{HttpApp, HttpRoutes}
3738
import org.http4s.client.blaze.BlazeClientBuilder
@@ -116,8 +117,8 @@ it is set up to create tags suitable for Datadog but you can use the helper func
116117

117118
```scala mdoc
118119
import cats.effect.IO
119-
import com.ovoenergy.effect.natchez.http4s.server.Configuration
120-
import com.ovoenergy.effect.natchez.http4s.server.Configuration.TagReader._
120+
import com.ovoenergy.effect.natchez.http4s.Configuration
121+
import com.ovoenergy.effect.natchez.http4s.Configuration.TagReader._
121122
import natchez.TraceValue.BooleanValue
122123
import cats.syntax.semigroup._
123124

natchez-http4s/src/main/scala/com/ovoenergy/effect/natchez/http4s/server/Configuration.scala renamed to natchez-http4s/src/main/scala/com/ovoenergy/effect/natchez/http4s/Configuration.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.ovoenergy.effect.natchez.http4s.server
1+
package com.ovoenergy.effect.natchez.http4s
22

33
import cats.Applicative
44
import cats.data.Kleisli
@@ -9,7 +9,7 @@ import cats.kernel.{Monoid, Semigroup}
99
import cats.syntax.foldable._
1010
import cats.syntax.functor._
1111
import cats.syntax.monoid._
12-
import com.ovoenergy.effect.natchez.http4s.server.Configuration.TagReader._
12+
import com.ovoenergy.effect.natchez.http4s.Configuration.TagReader.{MessageReader, RequestReader, ResponseReader}
1313
import natchez.TraceValue
1414
import natchez.TraceValue.StringValue
1515
import org.http4s.util.{CaseInsensitiveString, StringWriter}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.ovoenergy.effect.natchez.http4s.client
2+
3+
import cats.data.Kleisli
4+
import cats.effect.{Resource, Sync}
5+
import cats.syntax.functor._
6+
import cats.~>
7+
import com.ovoenergy.effect.natchez.http4s.Configuration
8+
import com.ovoenergy.effect.natchez.http4s.server.TraceMiddleware.removeNumericPathSegments
9+
import natchez.{Span, Trace}
10+
import org.http4s._
11+
import org.http4s.client.Client
12+
13+
trait TracedClient[F[_]] {
14+
def named(s: String): Client[F]
15+
}
16+
17+
object TracedClient {
18+
19+
type Traced[F[_], A] = Kleisli[F, Span[F], A]
20+
21+
private def dropTracing[F[_]](span: Span[F]): Traced[F, *] ~> F =
22+
Kleisli.applyK[F, Span[F]](span)
23+
24+
private def trace[F[_]]: F ~> Traced[F, *] =
25+
Kleisli.liftK
26+
27+
def apply[F[_]: Sync](client: Client[F], config: Configuration[F]): TracedClient[Traced[F, *]] =
28+
name => Client[Traced[F, *]] { req =>
29+
Resource(
30+
Trace[Traced[F, *]].span(s"$name:http.request:${removeNumericPathSegments(req.uri)}") {
31+
for {
32+
span <- Kleisli.ask[F, Span[F]]
33+
headers <- trace(span.kernel.map(_.toHeaders.map { case (k, v) => Header(k, v) }))
34+
withHeader = req.putHeaders(headers.toSeq: _*).mapK(dropTracing(span))
35+
reqTags <- trace(config.request.value.run(req.mapK(dropTracing(span))))
36+
_ <- trace(span.put(reqTags.toSeq:_*))
37+
(resp, rel) <- client.run(withHeader).mapK(trace[F]).map(_.mapK(trace)).allocated
38+
respTags <- trace(config.response.value.run(resp.mapK(dropTracing(span))))
39+
_ <- trace(span.put(respTags.toSeq:_*))
40+
} yield resp -> rel
41+
}
42+
)
43+
}
44+
}

natchez-http4s/src/main/scala/com/ovoenergy/effect/natchez/http4s/server/TraceMiddleware.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import natchez._
77
import org.http4s._
88
import cats.syntax.flatMap._
99
import cats.syntax.functor._
10+
import com.ovoenergy.effect.natchez.http4s.Configuration
1011

1112
object TraceMiddleware {
1213

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.ovoenergy.effect.natchez.http4s.client
2+
3+
import cats.data.Kleisli
4+
import cats.effect.Sync
5+
import cats.effect.concurrent.Ref
6+
import org.http4s.{Request, Response}
7+
import org.http4s.client.Client
8+
import cats.syntax.functor._
9+
10+
trait TestClient[F[_]] {
11+
def requests: F[List[Request[F]]]
12+
def client: Client[F]
13+
}
14+
15+
object TestClient {
16+
17+
def apply[F[_]: Sync]: F[TestClient[F]] =
18+
Ref.of[F, List[Request[F]]](List.empty).map { ref =>
19+
new TestClient[F] {
20+
def client: Client[F] =
21+
Client.fromHttpApp[F](Kleisli(r => ref.update(_ :+ r).as(Response[F]())))
22+
def requests: F[List[Request[F]]] =
23+
ref.get
24+
}
25+
}
26+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.ovoenergy.effect.natchez.http4s.client
2+
3+
import cats.data.Kleisli
4+
import cats.effect.{IO, Timer}
5+
import com.ovoenergy.effect.natchez.TestEntryPoint
6+
import com.ovoenergy.effect.natchez.TestEntryPoint.TestSpan
7+
import com.ovoenergy.effect.natchez.http4s.Configuration
8+
import natchez.{Kernel, Span}
9+
import org.http4s.Request
10+
import org.scalatest.matchers.should.Matchers
11+
import org.scalatest.wordspec.AnyWordSpec
12+
13+
import scala.concurrent.ExecutionContext.global
14+
15+
class TracedClientTest extends AnyWordSpec with Matchers {
16+
17+
implicit val timer: Timer[IO] = IO.timer(global)
18+
val unit: Kleisli[IO, Span[IO], Unit] = Kleisli.pure(())
19+
val config: Configuration[IO] = Configuration.default[IO]()
20+
type TraceIO[A] = Kleisli[IO, Span[IO], A]
21+
22+
"TracedClient" should {
23+
24+
"Add the kernel to requests" in {
25+
26+
val requests: List[Request[IO]] = (
27+
for {
28+
client <- TestClient[IO]
29+
ep <- TestEntryPoint[IO]
30+
http = TracedClient(client.client, config)
31+
kernel = Kernel(Map("X-Trace-Token" -> "token"))
32+
_ <- ep.continue("bar", kernel).use(http.named("foo").status(Request[TraceIO]()).run)
33+
reqs <- client.requests
34+
} yield reqs
35+
).unsafeRunSync
36+
37+
requests.forall(_.headers.exists(_.name.value === "X-Trace-Token")) shouldBe true
38+
}
39+
40+
"Create a new span for HTTP requests" in {
41+
42+
val spans: List[TestSpan] = (
43+
for {
44+
client <- TestClient[IO]
45+
ep <- TestEntryPoint[IO]
46+
http = TracedClient(client.client, config)
47+
_ <- ep.root("root").use(http.named("foo").status(Request[TraceIO]()).run)
48+
reqs <- ep.spans
49+
} yield reqs
50+
).unsafeRunSync
51+
52+
spans.length shouldBe 2
53+
spans.head.name shouldBe "foo:http.request:/"
54+
}
55+
}
56+
}

natchez-http4s/src/test/scala/com/ovoenergy/effect/natchez/http4s/server/TraceMiddlewareTest.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import cats.data.Kleisli
44
import cats.effect.concurrent.Ref
55
import cats.effect.{IO, Resource}
66
import cats.{Applicative, Monad}
7+
import com.ovoenergy.effect.natchez.http4s.Configuration
78
import fs2._
89
import natchez.TraceValue.{NumberValue, StringValue}
910
import natchez.{EntryPoint, Kernel, Span, TraceValue}

natchez-testkit/src/main/scala/com/ovoenergy/effect/natchez/TestEntryPoint.scala

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,33 +24,35 @@ object TestEntryPoint {
2424
exitCase: ExitCase[Throwable],
2525
parent: Option[String],
2626
completed: Instant,
27+
kernel: Kernel,
2728
name: String
2829
)
2930

3031
def apply[F[_]: Clock](implicit F: Sync[F]): F[TestEntryPoint[F]] =
3132
Ref.of[F, List[TestSpan]](List.empty).map { submitted =>
32-
def span(myName: String): Span[F] = new Span[F] {
33-
def span(name: String): Resource[F, Span[F]] = makeSpan(name, Some(myName))
33+
34+
def span(myName: String, k: Kernel): Span[F] = new Span[F] {
35+
def span(name: String): Resource[F, Span[F]] = makeSpan(name, Some(myName), k)
3436
def put(fields: (String, TraceValue)*): F[Unit] = F.unit
35-
def kernel: F[Kernel] = F.pure(Kernel(Map.empty))
37+
def kernel: F[Kernel] = F.pure(k)
3638
}
3739

38-
def makeSpan(name: String, parent: Option[String]): Resource[F, Span[F]] =
39-
Resource.makeCase(F.delay(span(name))) { (_, ec) =>
40+
def makeSpan(name: String, parent: Option[String], kernel: Kernel): Resource[F, Span[F]] =
41+
Resource.makeCase(F.delay(span(name, kernel))) { (_, ec) =>
4042
Clock[F]
4143
.realTime(TimeUnit.MILLISECONDS)
4244
.map(Instant.ofEpochMilli)
4345
.flatMap { time =>
44-
val span = TestSpan(ec, parent, time, name)
46+
val span = TestSpan(ec, parent, time, kernel, name)
4547
submitted.update(_ :+ span)
4648
}
4749
}
4850

4951
new TestEntryPoint[F] {
5052
def spans: F[List[TestSpan]] = submitted.get
51-
def root(name: String): Resource[F, Span[F]] = makeSpan(name, None)
52-
def continue(name: String, k: Kernel): Resource[F, Span[F]] = makeSpan(name, None)
53-
def continueOrElseRoot(name: String, k: Kernel): Resource[F, Span[F]] = makeSpan(name, None)
53+
def root(name: String): Resource[F, Span[F]] = makeSpan(name, None, Kernel(Map.empty))
54+
def continue(name: String, k: Kernel): Resource[F, Span[F]] = makeSpan(name, None, k)
55+
def continueOrElseRoot(name: String, k: Kernel): Resource[F, Span[F]] = makeSpan(name, None, k)
5456
}
5557
}
5658
}

0 commit comments

Comments
 (0)