diff --git a/build.sbt b/build.sbt index 67bcdc6..9ed1c60 100644 --- a/build.sbt +++ b/build.sbt @@ -2,9 +2,9 @@ ThisBuild / tlBaseVersion := "0.6" val http4sVersion = "0.23.27" val natchezVersion = "0.3.6" -val scala212Version = "2.12.19" +val scala212Version = "2.12.20" val scala213Version = "2.13.15" -val scala3Version = "3.3.3" +val scala3Version = "3.3.4" val slf4jVersion = "2.0.16" val munitCEVersion = "2.0.0" val scalacheckEffectVersion = "2.0.0-M2" diff --git a/modules/http4s/src/main/scala/natchez/http4s/NatchezMiddleware.scala b/modules/http4s/src/main/scala/natchez/http4s/NatchezMiddleware.scala index 6736dd8..39a0649 100644 --- a/modules/http4s/src/main/scala/natchez/http4s/NatchezMiddleware.scala +++ b/modules/http4s/src/main/scala/natchez/http4s/NatchezMiddleware.scala @@ -9,7 +9,7 @@ import cats.syntax.all._ import cats.effect.{MonadCancel, MonadCancelThrow, Outcome, Resource} import cats.effect.syntax.all._ import Outcome._ -import natchez.{Tags, Trace, TraceValue} +import natchez.{Span, Tags, Trace, TraceValue} import natchez.Span.Options.Defaults import natchez.Span.SpanKind import org.http4s.client.Client @@ -114,6 +114,42 @@ object NatchezMiddleware { (additionalAttributes: (String, TraceValue)*): Client[F] = NatchezMiddleware.client(client, (_: Request[F]) => additionalAttributes.pure[F]) + /** + * A middleware that adds the current span's kernel to outgoing requests, performs requests in + * a span called `http4s-client-request`, and adds the following fields to that span. + * + * - "client.http.method" -> "GET", "PUT", etc. + * - "client.http.uri" -> request URI + * - "client.http.status_code" -> "200", "403", etc. // why is this a string? + * + * @param client the `Client[F]` to be enhanced + * @param additionalAttributes additional attributes to be added to the span + * @tparam F An effect with instances of `Trace[F]` and `MonadCancelThrow[F]` + * @return the enhanced `Client[F]` + */ + def clientWithAttributes[F[_] : Trace : MonadCancelThrow](client: Client[F], + spanOptions: Span.Options) + (additionalAttributes: (String, TraceValue)*): Client[F] = + NatchezMiddleware.client(client, spanOptions, (_: Request[F]) => additionalAttributes.pure[F]) + + /** + * A middleware that adds the current span's kernel to outgoing requests, performs requests in + * a span called `http4s-client-request`, and adds the following fields to that span. + * + * - "client.http.method" -> "GET", "PUT", etc. + * - "client.http.uri" -> request URI + * - "client.http.status_code" -> "200", "403", etc. // why is this a string? + * + * @param client the `Client[F]` to be enhanced + * @param additionalAttributesF a function that takes the `Request[F]` and returns any additional attributes to be added to the span + * @tparam F An effect with instances of `Trace[F]` and `MonadCancelThrow[F]` + * @return the enhanced `Client[F]` + */ + def client[F[_] : Trace : MonadCancelThrow](client: Client[F], + additionalAttributesF: Request[F] => F[Seq[(String, TraceValue)]], + ): Client[F] = + NatchezMiddleware.client(client, Defaults.withSpanKind(SpanKind.Client), additionalAttributesF) + /** * A middleware that adds the current span's kernel to outgoing requests, performs requests in * a span called `http4s-client-request`, and adds the following fields to that span. @@ -128,11 +164,12 @@ object NatchezMiddleware { * @return the enhanced `Client[F]` */ def client[F[_] : Trace : MonadCancelThrow](client: Client[F], + spanOptions: Span.Options, additionalAttributesF: Request[F] => F[Seq[(String, TraceValue)]], ): Client[F] = Client { req => Resource.applyFull {poll => - Trace[F].span("http4s-client-request", Defaults.withSpanKind(SpanKind.Client)) { + Trace[F].span("http4s-client-request", spanOptions) { for { knl <- Trace[F].kernel _ <- Trace[F].put( diff --git a/modules/http4s/src/test/scala/natchez/http4s/NatchezMiddlewareSuite.scala b/modules/http4s/src/test/scala/natchez/http4s/NatchezMiddlewareSuite.scala index c0a7e50..ba573ef 100644 --- a/modules/http4s/src/test/scala/natchez/http4s/NatchezMiddlewareSuite.scala +++ b/modules/http4s/src/test/scala/natchez/http4s/NatchezMiddlewareSuite.scala @@ -8,19 +8,20 @@ import cats.Monad import cats.data.{Chain, Kleisli} import cats.effect.{IO, MonadCancelThrow, Resource} import munit.ScalaCheckEffectSuite +import natchez.Span.Options.SpanCreationPolicy import natchez.Span.SpanKind -import natchez.{InMemory, Kernel, Span, Trace, TraceValue} import natchez.TraceValue.StringValue -import natchez.http4s.syntax.entrypoint._ -import org.http4s._ -import org.http4s.headers._ +import natchez.http4s.syntax.entrypoint.* +import natchez.* +import org.http4s.* import org.http4s.client.Client -import org.http4s.dsl.request._ -import org.http4s.syntax.literals._ -import org.scalacheck.{Arbitrary, Gen} +import org.http4s.dsl.request.* +import org.http4s.headers.* +import org.http4s.syntax.literals.* import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.effect.PropF -import org.typelevel.ci._ +import org.scalacheck.{Arbitrary, Gen} +import org.typelevel.ci.* class NatchezMiddlewareSuite extends InMemorySuite @@ -44,6 +45,47 @@ class NatchezMiddlewareSuite } yield key -> value } + private implicit val arbCIString: Arbitrary[CIString] = Arbitrary { + Gen.alphaLowerStr.map(CIString(_)) + } + + private implicit val arbKernel: Arbitrary[Kernel] = Arbitrary { + arbitrary[Map[CIString, String]].map(Kernel(_)) + } + + private implicit val arbSpanCreationPolicy: Arbitrary[SpanCreationPolicy] = Arbitrary { + Gen.oneOf(SpanCreationPolicy.Default, SpanCreationPolicy.Coalesce, SpanCreationPolicy.Suppress) + } + + private implicit val arbSpanKind: Arbitrary[SpanKind] = Arbitrary { + Gen.oneOf( + SpanKind.Internal, + SpanKind.Client, + SpanKind.Server, + SpanKind.Producer, + SpanKind.Consumer, + ) + } + + private implicit val arbSpanOptions: Arbitrary[Span.Options] = Arbitrary { + for { + parentKernel <- arbitrary[Option[Kernel]] + spanCreationPolicy <- arbitrary[SpanCreationPolicy] + spanKind <- arbitrary[SpanKind] + links <- arbitrary[List[Kernel]].map(Chain.fromSeq) + } yield { + links.foldLeft { + parentKernel.foldLeft { + Span + .Options + .Defaults + .withSpanKind(spanKind) + .withSpanCreationPolicy(spanCreationPolicy) + }(_.withParentKernel(_)) + }(_.withLink(_)) + } + } + test("do not leak security and payload headers to the client request") { val headers = Headers( // security @@ -76,7 +118,7 @@ class NatchezMiddlewareSuite for { ref <- IO.ref(Chain.empty[(Lineage, NatchezCommand)]) ep <- IO.pure(new InMemory.EntryPoint(ref)) - routes <- IO.pure(ep.liftT(httpRoutes[Kleisli[IO, natchez.Span[IO], *]]())) + routes <- IO.pure(ep.liftT(httpRoutes[Kleisli[IO, natchez.Span[IO], *]](None))) response <- routes.orNotFound.run(request) } yield { assertEquals(response.status.code, 200) @@ -85,7 +127,8 @@ class NatchezMiddlewareSuite } test("generate proper tracing history") { - PropF.forAllF { (userSpecifiedTags: List[(String, TraceValue)]) => + PropF.forAllF { (userSpecifiedTags: List[(String, TraceValue)], + maybeSpanOptions: Option[Span.Options]) => val request = Request[IO]( method = Method.GET, uri = uri"/hello/some-name", @@ -118,10 +161,13 @@ class NatchezMiddlewareSuite "http.status_code" -> StringValue("200") ) + val spanOptions = maybeSpanOptions.getOrElse(Span.Options.Defaults.withSpanKind(SpanKind.Client)) + val kernel = maybeSpanOptions.flatMap(_.parentKernel) + List( (Lineage.Root, NatchezCommand.CreateRootSpan("/hello/some-name", requestKernel, Span.Options.Defaults)), (Lineage.Root("/hello/some-name"), NatchezCommand.CreateSpan("call-proxy", None, Span.Options.Defaults)), - (Lineage.Root("/hello/some-name") / "call-proxy", NatchezCommand.CreateSpan("http4s-client-request", None, Span.Options.Defaults.withSpanKind(SpanKind.Client))), + (Lineage.Root("/hello/some-name") / "call-proxy", NatchezCommand.CreateSpan("http4s-client-request", kernel, spanOptions)), (Lineage.Root("/hello/some-name") / "call-proxy" / "http4s-client-request", NatchezCommand.AskKernel(requestKernel)), (Lineage.Root("/hello/some-name") / "call-proxy" / "http4s-client-request", NatchezCommand.Put(clientRequestTags)), (Lineage.Root("/hello/some-name") / "call-proxy" / "http4s-client-request", NatchezCommand.Put(userSpecifiedTags)), @@ -136,15 +182,22 @@ class NatchezMiddlewareSuite for { ep <- InMemory.EntryPoint.create[IO] - routes <- IO.pure(ep.liftT(httpRoutes[Kleisli[IO, natchez.Span[IO], *]](userSpecifiedTags: _*))) + routes <- IO.pure(ep.liftT(httpRoutes[Kleisli[IO, natchez.Span[IO], *]](maybeSpanOptions, userSpecifiedTags *))) _ <- routes.orNotFound.run(request) history <- ep.ref.get } yield assertEquals(history.toList, expectedHistory) } } - private def httpRoutes[F[_]: MonadCancelThrow: Trace](additionalAttributes: (String, TraceValue)*): HttpRoutes[F] = { - val client = NatchezMiddleware.clientWithAttributes(echoHeadersClient[F])(additionalAttributes: _*) + private def httpRoutes[F[_]: MonadCancelThrow: Trace](maybeSpanOptions: Option[Span.Options], + additionalAttributes: (String, TraceValue)*): HttpRoutes[F] = { + val client = maybeSpanOptions match { + case Some(spanOptions) => + NatchezMiddleware.clientWithAttributes(echoHeadersClient[F], spanOptions)(additionalAttributes *) + case None => + NatchezMiddleware.clientWithAttributes(echoHeadersClient[F])(additionalAttributes *) + } + val server = NatchezMiddleware.server(proxyRoutes(client)) server }