diff --git a/build.sbt b/build.sbt index d658584..eb8be54 100644 --- a/build.sbt +++ b/build.sbt @@ -49,6 +49,7 @@ lazy val `http4s-otel4s-middleware` = tlCrossRootProject lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file("core")) + .enablePlugins(BuildInfoPlugin) .settings(sharedSettings) .settings( name := s"$baseName-core", @@ -57,6 +58,9 @@ lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform) "org.typelevel" %%% "otel4s-core-common" % otel4sV, "org.typelevel" %%% "otel4s-semconv" % otel4sV, ), + buildInfoKeys := Seq(version), + buildInfoPackage := "org.http4s.otel4s.middleware", + buildInfoOptions += BuildInfoOption.PackagePrivate, ) lazy val metrics = crossProject(JVMPlatform, JSPlatform, NativePlatform) diff --git a/examples/src/main/scala/example/Http4sExample.scala b/examples/src/main/scala/example/Http4sExample.scala index 90269c7..29e7d26 100644 --- a/examples/src/main/scala/example/Http4sExample.scala +++ b/examples/src/main/scala/example/Http4sExample.scala @@ -34,9 +34,10 @@ import org.http4s.otel4s.middleware.trace.server.ServerMiddlewareBuilder import org.http4s.server.Server import org.http4s.server.middleware.Metrics import org.typelevel.otel4s.Otel4s -import org.typelevel.otel4s.metrics.Meter +import org.typelevel.otel4s.metrics.MeterProvider import org.typelevel.otel4s.oteljava.OtelJava import org.typelevel.otel4s.trace.Tracer +import org.typelevel.otel4s.trace.TracerProvider /** Start up Jaeger thus: * @@ -73,20 +74,21 @@ object Http4sExample extends IOApp with Common { def tracer[F[_]](otel: Otel4s[F]): F[Tracer[F]] = otel.tracerProvider.tracer("Http4sExample").get - def meter[F[_]](otel: Otel4s[F]): F[Meter[F]] = - otel.meterProvider.meter("Http4sExample").get - // Our main app resource - def server[F[_]: Async: Network: Tracer: Meter]: Resource[F, Server] = + def server[F[_]: Async: Network: TracerProvider: Tracer: MeterProvider]: Resource[F, Server] = for { + clientMiddleware <- ClientMiddlewareBuilder.default(redactor).build.toResource client <- EmberClientBuilder .default[F] .build - .map(ClientMiddlewareBuilder.default(redactor).build) + .map(clientMiddleware) metricsOps <- OtelMetrics.serverMetricsOps[F]().toResource - app = ServerMiddlewareBuilder.default[F](redactor).buildHttpApp { - Metrics(metricsOps)(routes(client)).orNotFound - } + app <- ServerMiddlewareBuilder + .default[F](redactor) + .buildHttpApp { + Metrics(metricsOps)(routes(client)).orNotFound + } + .toResource sv <- EmberServerBuilder.default[F].withPort(port"8080").withHttpApp(app).build } yield sv @@ -95,10 +97,10 @@ object Http4sExample extends IOApp with Common { OtelJava .autoConfigured[IO]() .flatMap { otel4s => + implicit val TP: TracerProvider[IO] = otel4s.tracerProvider + implicit val MP: MeterProvider[IO] = otel4s.meterProvider Resource.eval(tracer(otel4s)).flatMap { implicit T: Tracer[IO] => - Resource.eval(meter(otel4s)).flatMap { implicit M: Meter[IO] => - server[IO] - } + server[IO] } } .use(_ => IO.never) diff --git a/metrics/src/main/scala/org/http4s/otel4s/middleware/metrics/OtelMetrics.scala b/metrics/src/main/scala/org/http4s/otel4s/middleware/metrics/OtelMetrics.scala index ae36b74..8ab7ae0 100644 --- a/metrics/src/main/scala/org/http4s/otel4s/middleware/metrics/OtelMetrics.scala +++ b/metrics/src/main/scala/org/http4s/otel4s/middleware/metrics/OtelMetrics.scala @@ -47,7 +47,7 @@ object OtelMetrics { * @param attributes additional [[org.typelevel.otel4s.Attributes]] that are added to all metrics * @param responseDurationSecondsHistogramBuckets histogram buckets for the response duration metrics */ - def clientMetricsOps[F[_]: Monad: Meter]( + def clientMetricsOps[F[_]: Monad: MeterProvider]( attributes: Attributes = Attributes.empty, responseDurationSecondsHistogramBuckets: BucketBoundaries = DefaultHistogramBuckets, ): F[MetricsOps[F]] = @@ -72,7 +72,7 @@ object OtelMetrics { * @param attributes additional [[org.typelevel.otel4s.Attributes]] that are added to all metrics * @param responseDurationSecondsHistogramBuckets histogram buckets for the response duration metrics */ - def serverMetricsOps[F[_]: Monad: Meter]( + def serverMetricsOps[F[_]: Monad: MeterProvider]( attributes: Attributes = Attributes.empty, responseDurationSecondsHistogramBuckets: BucketBoundaries = DefaultHistogramBuckets, ): F[MetricsOps[F]] = @@ -82,16 +82,23 @@ object OtelMetrics { responseDurationSecondsHistogramBuckets, ) - private def metricsOps[F[_]: Monad: Meter]( + private def metricsOps[F[_]: Monad: MeterProvider]( kind: String, attributes: Attributes, responseDurationSecondsHistogramBuckets: BucketBoundaries, ): F[MetricsOps[F]] = for { - metrics <- createMetricsCollection( - kind, - responseDurationSecondsHistogramBuckets, - ) + meter <- MeterProvider[F] + .meter(s"org.http4s.otel4s.middleware.$kind") + .withVersion(org.http4s.otel4s.middleware.BuildInfo.version) + .get + metrics <- { + implicit val M: Meter[F] = meter + createMetricsCollection( + kind, + responseDurationSecondsHistogramBuckets, + ) + } } yield createMetricsOps( metrics, attributes, diff --git a/metrics/src/test/scala/org/http4s/otel4s/middleware/metrics/OtelMetricsTests.scala b/metrics/src/test/scala/org/http4s/otel4s/middleware/metrics/OtelMetricsTests.scala index 9da0afd..eaa7c41 100644 --- a/metrics/src/test/scala/org/http4s/otel4s/middleware/metrics/OtelMetricsTests.scala +++ b/metrics/src/test/scala/org/http4s/otel4s/middleware/metrics/OtelMetricsTests.scala @@ -22,7 +22,7 @@ import cats.effect.IO import munit.CatsEffectSuite import org.http4s.server.middleware.Metrics import org.typelevel.otel4s.Attributes -import org.typelevel.otel4s.metrics.Meter +import org.typelevel.otel4s.metrics.MeterProvider import org.typelevel.otel4s.sdk.metrics.data.MetricPoints import org.typelevel.otel4s.sdk.metrics.data.PointData import org.typelevel.otel4s.sdk.testkit.metrics.MetricsTestkit @@ -33,9 +33,8 @@ class OtelMetricsTests extends CatsEffectSuite { .inMemory[IO]() .use { testkit => for { - meterIO <- testkit.meterProvider.get("meter") metricsOps <- { - implicit val meter: Meter[IO] = meterIO + implicit val MP: MeterProvider[IO] = testkit.meterProvider OtelMetrics.serverMetricsOps[IO]() } _ <- { diff --git a/project/plugins.sbt b/project/plugins.sbt index d02e268..1042cae 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,5 @@ addSbtPlugin("org.http4s" % "sbt-http4s-org" % "0.17.5") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") diff --git a/trace/client/src/main/scala/org/http4s/otel4s/middleware/trace/client/ClientMiddlewareBuilder.scala b/trace/client/src/main/scala/org/http4s/otel4s/middleware/trace/client/ClientMiddlewareBuilder.scala index 9785196..89ef86f 100644 --- a/trace/client/src/main/scala/org/http4s/otel4s/middleware/trace/client/ClientMiddlewareBuilder.scala +++ b/trace/client/src/main/scala/org/http4s/otel4s/middleware/trace/client/ClientMiddlewareBuilder.scala @@ -25,17 +25,18 @@ import cats.effect.Outcome import cats.effect.Resource import cats.syntax.applicative._ import cats.syntax.flatMap._ +import cats.syntax.functor._ import fs2.Stream import org.http4s.client.Client import org.typelevel.otel4s.trace.SpanKind import org.typelevel.otel4s.trace.StatusCode -import org.typelevel.otel4s.trace.Tracer +import org.typelevel.otel4s.trace.TracerProvider /** Middleware builder for wrapping an http4s `Client` to add tracing. * * @see [[https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client]] */ -class ClientMiddlewareBuilder[F[_]: Tracer: Concurrent] private ( +class ClientMiddlewareBuilder[F[_]: TracerProvider: Concurrent] private ( urlRedactor: UriRedactor, spanDataProvider: SpanDataProvider, urlTemplateClassifier: UriTemplateClassifier, @@ -81,79 +82,87 @@ class ClientMiddlewareBuilder[F[_]: Tracer: Concurrent] private ( copy(shouldTrace = shouldTrace) /** @return the configured middleware */ - def build: Client[F] => Client[F] = (client: Client[F]) => - Client[F] { (req: Request[F]) => // Resource[F, Response[F]] - if ( - !shouldTrace(req.requestPrelude).shouldTrace || - !Tracer[F].meta.isEnabled - ) { - client.run(req) - } else { - val reqNoBody = req.withBodyStream(Stream.empty) - val shared = - spanDataProvider.processSharedData( - reqNoBody, - urlTemplateClassifier, - urlRedactor, - ) - val spanName = - spanDataProvider.spanName( - reqNoBody, - urlTemplateClassifier, - urlRedactor, - shared, - ) - val reqAttributes = - spanDataProvider.requestAttributes( - reqNoBody, - urlTemplateClassifier, - urlRedactor, - shared, - headersAllowedAsAttributes.request, - ) + def build: F[Client[F] => Client[F]] = + for { + tracer <- TracerProvider[F] + .tracer("org.http4s.otel4s.middleware.client") + .withVersion(org.http4s.otel4s.middleware.BuildInfo.version) + .get + } yield (client: Client[F]) => + Client[F] { (req: Request[F]) => // Resource[F, Response[F]] + if ( + !shouldTrace(req.requestPrelude).shouldTrace || + !tracer.meta.isEnabled + ) { + client.run(req) + } else { + val reqNoBody = req.withBodyStream(Stream.empty) + val shared = + spanDataProvider.processSharedData( + reqNoBody, + urlTemplateClassifier, + urlRedactor, + ) + val spanName = + spanDataProvider.spanName( + reqNoBody, + urlTemplateClassifier, + urlRedactor, + shared, + ) + val reqAttributes = + spanDataProvider.requestAttributes( + reqNoBody, + urlTemplateClassifier, + urlRedactor, + shared, + headersAllowedAsAttributes.request, + ) - MonadCancelThrow[Resource[F, *]].uncancelable { poll => - for { - res <- Tracer[F] - .spanBuilder(spanName) - .withSpanKind(SpanKind.Client) - .addAttributes(reqAttributes) - .build - .resource - span = res.span - trace = res.trace - traceHeaders <- Resource.eval(Tracer[F].propagate(Headers.empty)).mapK(trace) - newReq = req.withHeaders(traceHeaders ++ req.headers) + MonadCancelThrow[Resource[F, *]].uncancelable { poll => + for { + res <- tracer + .spanBuilder(spanName) + .withSpanKind(SpanKind.Client) + .addAttributes(reqAttributes) + .build + .resource + span = res.span + trace = res.trace + traceHeaders <- Resource.eval(tracer.propagate(Headers.empty)).mapK(trace) + newReq = req.withHeaders(traceHeaders ++ req.headers) - resp <- poll(client.run(newReq).mapK(trace)).guaranteeCase { - case Outcome.Succeeded(fa) => - fa.evalMap { resp => - val respAttributes = - spanDataProvider.responseAttributes( - resp.withBodyStream(Stream.empty), - headersAllowedAsAttributes.response, - ) - span.addAttributes(respAttributes) >> span - .setStatus(StatusCode.Error) - .unlessA(resp.status.isSuccess) - } + resp <- poll(client.run(newReq).mapK(trace)).guaranteeCase { + case Outcome.Succeeded(fa) => + fa.evalMap { resp => + val respAttributes = + spanDataProvider.responseAttributes( + resp.withBodyStream(Stream.empty), + headersAllowedAsAttributes.response, + ) + span.addAttributes(respAttributes) >> span + .setStatus(StatusCode.Error) + .unlessA(resp.status.isSuccess) + } - case Outcome.Errored(e) => - Resource.eval(span.addAttributes(TypedAttributes.errorType(e))) + case Outcome.Errored(e) => + Resource.eval(span.addAttributes(TypedAttributes.errorType(e))) - case Outcome.Canceled() => - Resource.unit - } - } yield resp + case Outcome.Canceled() => + Resource.unit + } + } yield resp + } } } - } } object ClientMiddlewareBuilder { /** @return a client middleware builder with default configuration */ - def default[F[_]: Tracer: Concurrent](urlRedactor: UriRedactor): ClientMiddlewareBuilder[F] = + def default[F[_]: TracerProvider: Concurrent]( + urlRedactor: UriRedactor + ): ClientMiddlewareBuilder[F] = new ClientMiddlewareBuilder[F]( urlRedactor, Defaults.spanDataProvider, diff --git a/trace/client/src/test/scala/org/http4s/otel4s/middleware/trace/client/ClientMiddlewareTests.scala b/trace/client/src/test/scala/org/http4s/otel4s/middleware/trace/client/ClientMiddlewareTests.scala index 74753ed..a03a900 100644 --- a/trace/client/src/test/scala/org/http4s/otel4s/middleware/trace/client/ClientMiddlewareTests.scala +++ b/trace/client/src/test/scala/org/http4s/otel4s/middleware/trace/client/ClientMiddlewareTests.scala @@ -38,6 +38,7 @@ import org.typelevel.otel4s.sdk.trace.data.StatusData import org.typelevel.otel4s.trace.SpanKind import org.typelevel.otel4s.trace.StatusCode import org.typelevel.otel4s.trace.Tracer +import org.typelevel.otel4s.trace.TracerProvider import scala.concurrent.duration.Duration import scala.util.control.NoStackTrace @@ -52,9 +53,19 @@ class ClientMiddlewareTests extends CatsEffectSuite { .inMemory[IO]() .use { testkit => for { - tracerIO <- testkit.tracerProvider.get("tracer") + clientMiddleware <- { + implicit val TP: TracerProvider[IO] = testkit.tracerProvider + ClientMiddlewareBuilder + .default[IO](MinimalRedactor) + .withHeadersAllowedAsAttributes( + HeadersAllowedAsAttributes( + request = Set(ci"foo"), + response = Set(ci"baz"), + ) + ) + .build + } _ <- { - implicit val tracer: Tracer[IO] = tracerIO val headers = Headers(Header.Raw(ci"foo", "bar"), Header.Raw(ci"baz", "qux")) val response = Response[IO](Status.Ok).withHeaders(headers) @@ -62,17 +73,7 @@ class ClientMiddlewareTests extends CatsEffectSuite { Client.fromHttpApp[IO] { HttpApp[IO](_.body.compile.drain.as(response)) } - val tracedClient = - ClientMiddlewareBuilder - .default[IO](MinimalRedactor) - .withHeadersAllowedAsAttributes( - HeadersAllowedAsAttributes( - request = Set(ci"foo"), - response = Set(ci"baz"), - ) - ) - .build(fakeClient) - + val tracedClient = clientMiddleware(fakeClient) val request = Request[IO](Method.GET, uri"http://localhost/?#") .withHeaders(headers) @@ -111,6 +112,10 @@ class ClientMiddlewareTests extends CatsEffectSuite { .inMemory[IO]() .use { testkit => for { + clientMiddleware <- { + implicit val TP: TracerProvider[IO] = testkit.tracerProvider + ClientMiddlewareBuilder.default[IO](MinimalRedactor).build + } tracerIO <- testkit.tracerProvider.get("tracer") _ <- { implicit val tracer: Tracer[IO] = tracerIO @@ -124,9 +129,7 @@ class ClientMiddlewareTests extends CatsEffectSuite { .run(req) .evalTap(_ => Tracer[IO].currentSpanOrThrow.flatMap(_.updateName("NEW SPAN NAME"))) } - val tracedClient = - ClientMiddlewareBuilder.default[IO](MinimalRedactor).build(traceManipulatingClient) - + val tracedClient = clientMiddleware(traceManipulatingClient) val request = Request[IO](Method.GET, uri"http://localhost/?#") tracedClient.run(request).use(_.body.compile.drain) } @@ -175,20 +178,20 @@ class ClientMiddlewareTests extends CatsEffectSuite { .inMemory[IO]() .use { testkit => for { - tracerIO <- testkit.tracerProvider.get("tracer") + clientMiddleware <- { + implicit val TP: TracerProvider[IO] = testkit.tracerProvider + ClientMiddlewareBuilder + .default[IO](MinimalRedactor) + .withSpanDataProvider(provider) + .build + } _ <- { - implicit val tracer: Tracer[IO] = tracerIO val response = Response[IO](Status.Ok) val fakeClient = Client.fromHttpApp[IO] { HttpApp[IO](_.body.compile.drain.as(response)) } - val tracedClient = - ClientMiddlewareBuilder - .default[IO](MinimalRedactor) - .withSpanDataProvider(provider) - .build(fakeClient) - + val tracedClient = clientMiddleware(fakeClient) val request = Request[IO](Method.GET, uri"http://localhost/?#") tracedClient.run(request).use(_.body.compile.drain) } @@ -206,48 +209,54 @@ class ClientMiddlewareTests extends CatsEffectSuite { TracesTestkit .inMemory[IO]() .use { testkit => - testkit.tracerProvider.get("tracer").flatMap { implicit tracer => - val error = new RuntimeException("oops") with NoStackTrace {} + implicit val TP: TracerProvider[IO] = testkit.tracerProvider + ClientMiddlewareBuilder + .default[IO](MinimalRedactor) + .build + .flatMap { clientMiddleware => + val error = new RuntimeException("oops") with NoStackTrace {} + + val fakeClient = Client { (_: Request[IO]) => + Resource.raiseError[IO, Response[IO], Throwable](error) + } - val fakeClient = Client { (_: Request[IO]) => - Resource.raiseError[IO, Response[IO], Throwable](error) - } + val tracedClient = clientMiddleware(fakeClient) + val request = Request[IO](Method.GET, uri"http://localhost/") + + val events = Vector( + EventData.fromException( + Duration.Zero, + error, + LimitedData + .attributes( + spanLimits.maxNumberOfAttributes, + spanLimits.maxAttributeValueLength, + ), + escaped = false, + ) + ) + + val status = StatusData(StatusCode.Error) - val tracedClient = - ClientMiddlewareBuilder.default[IO](MinimalRedactor).build(fakeClient) - val request = Request[IO](Method.GET, uri"http://localhost/") - - val events = Vector( - EventData.fromException( - Duration.Zero, - error, - LimitedData - .attributes(spanLimits.maxNumberOfAttributes, spanLimits.maxAttributeValueLength), - escaped = false, + val attributes = Attributes( + Attribute("error.type", error.getClass.getName), + Attribute("http.request.method", "GET"), + Attribute("network.protocol.version", "1.1"), + Attribute("server.address", "localhost"), + Attribute("server.port", 80L), + Attribute("url.full", "http://localhost/"), + Attribute("url.scheme", "http"), ) - ) - - val status = StatusData(StatusCode.Error) - - val attributes = Attributes( - Attribute("error.type", error.getClass.getName), - Attribute("http.request.method", "GET"), - Attribute("network.protocol.version", "1.1"), - Attribute("server.address", "localhost"), - Attribute("server.port", 80L), - Attribute("url.full", "http://localhost/"), - Attribute("url.scheme", "http"), - ) - - for { - _ <- tracedClient.run(request).use_.attempt - spans <- testkit.finishedSpans - } yield { - assertEquals(spans.map(_.attributes.elements), List(attributes)) - assertEquals(spans.map(_.events.elements), List(events)) - assertEquals(spans.map(_.status), List(status)) + + for { + _ <- tracedClient.run(request).use_.attempt + spans <- testkit.finishedSpans + } yield { + assertEquals(spans.map(_.attributes.elements), List(attributes)) + assertEquals(spans.map(_.events.elements), List(events)) + assertEquals(spans.map(_.status), List(status)) + } } - } } } } @@ -257,26 +266,29 @@ class ClientMiddlewareTests extends CatsEffectSuite { TracesTestkit .inMemory[IO]() .use { testkit => - testkit.tracerProvider.get("tracer").flatMap { implicit tracer => - val fakeClient = Client { (_: Request[IO]) => - Resource.canceled[IO] >> Resource.never[IO, Response[IO]] - } + implicit val TP: TracerProvider[IO] = testkit.tracerProvider + ClientMiddlewareBuilder + .default[IO](MinimalRedactor) + .build + .flatMap { clientMiddleware => + val fakeClient = Client { (_: Request[IO]) => + Resource.canceled[IO] >> Resource.never[IO, Response[IO]] + } - val tracedClient = - ClientMiddlewareBuilder.default[IO](MinimalRedactor).build(fakeClient) - val request = Request[IO](Method.GET, uri"http://localhost/?#") + val tracedClient = clientMiddleware(fakeClient) + val request = Request[IO](Method.GET, uri"http://localhost/?#") - val status = StatusData(StatusCode.Error, "canceled") + val status = StatusData(StatusCode.Error, "canceled") - for { - f <- tracedClient.run(request).use_.start - _ <- f.joinWithUnit - spans <- testkit.finishedSpans - } yield { - assertEquals(spans.flatMap(_.events.elements), Nil) - assertEquals(spans.map(_.status), List(status)) + for { + f <- tracedClient.run(request).use_.start + _ <- f.joinWithUnit + spans <- testkit.finishedSpans + } yield { + assertEquals(spans.flatMap(_.events.elements), Nil) + assertEquals(spans.map(_.status), List(status)) + } } - } } } } @@ -286,49 +298,55 @@ class ClientMiddlewareTests extends CatsEffectSuite { TracesTestkit .inMemory[IO]() .use { testkit => - testkit.tracerProvider.get("tracer").flatMap { implicit tracer => - val error = new RuntimeException("oops") with NoStackTrace {} + implicit val TP: TracerProvider[IO] = testkit.tracerProvider + ClientMiddlewareBuilder + .default[IO](MinimalRedactor) + .build + .flatMap { clientMiddleware => + val error = new RuntimeException("oops") with NoStackTrace {} + + val fakeClient = + Client.fromHttpApp[IO] { + HttpApp[IO](_.body.compile.drain.as(Response[IO](Status.Ok))) + } + + val tracedClient = clientMiddleware(fakeClient) + val request = Request[IO](Method.GET, uri"http://localhost/") + + val events = Vector( + EventData.fromException( + Duration.Zero, + error, + LimitedData + .attributes( + spanLimits.maxNumberOfAttributes, + spanLimits.maxAttributeValueLength, + ), + escaped = false, + ) + ) - val fakeClient = - Client.fromHttpApp[IO] { - HttpApp[IO](_.body.compile.drain.as(Response[IO](Status.Ok))) - } + val status = StatusData(StatusCode.Error) - val tracedClient = - ClientMiddlewareBuilder.default[IO](MinimalRedactor).build(fakeClient) - val request = Request[IO](Method.GET, uri"http://localhost/") - - val events = Vector( - EventData.fromException( - Duration.Zero, - error, - LimitedData - .attributes(spanLimits.maxNumberOfAttributes, spanLimits.maxAttributeValueLength), - escaped = false, + val attributes = Attributes( + Attribute("http.request.method", "GET"), + Attribute("http.response.status_code", 200L), + Attribute("network.protocol.version", "1.1"), + Attribute("server.address", "localhost"), + Attribute("server.port", 80L), + Attribute("url.full", "http://localhost/"), + Attribute("url.scheme", "http"), ) - ) - - val status = StatusData(StatusCode.Error) - - val attributes = Attributes( - Attribute("http.request.method", "GET"), - Attribute("http.response.status_code", 200L), - Attribute("network.protocol.version", "1.1"), - Attribute("server.address", "localhost"), - Attribute("server.port", 80L), - Attribute("url.full", "http://localhost/"), - Attribute("url.scheme", "http"), - ) - - for { - _ <- tracedClient.run(request).surround(IO.raiseError(error)).attempt - spans <- testkit.finishedSpans - } yield { - assertEquals(spans.map(_.attributes.elements), List(attributes)) - assertEquals(spans.map(_.events.elements), List(events)) - assertEquals(spans.map(_.status), List(status)) + + for { + _ <- tracedClient.run(request).surround(IO.raiseError(error)).attempt + spans <- testkit.finishedSpans + } yield { + assertEquals(spans.map(_.attributes.elements), List(attributes)) + assertEquals(spans.map(_.events.elements), List(events)) + assertEquals(spans.map(_.status), List(status)) + } } - } } } } @@ -338,27 +356,30 @@ class ClientMiddlewareTests extends CatsEffectSuite { TracesTestkit .inMemory[IO]() .use { testkit => - testkit.tracerProvider.get("tracer").flatMap { implicit tracer => - val fakeClient = - Client.fromHttpApp[IO] { - HttpApp[IO](_.body.compile.drain.as(Response[IO](Status.Ok))) + implicit val TP: TracerProvider[IO] = testkit.tracerProvider + ClientMiddlewareBuilder + .default[IO](MinimalRedactor) + .build + .flatMap { clientMiddleware => + val fakeClient = + Client.fromHttpApp[IO] { + HttpApp[IO](_.body.compile.drain.as(Response[IO](Status.Ok))) + } + + val tracedClient = clientMiddleware(fakeClient) + val request = Request[IO](Method.GET, uri"http://localhost/?#") + + val status = StatusData(StatusCode.Error, "canceled") + + for { + f <- tracedClient.run(request).surround(IO.canceled).start + _ <- f.joinWithUnit + spans <- testkit.finishedSpans + } yield { + assertEquals(spans.flatMap(_.events.elements), Nil) + assertEquals(spans.map(_.status), List(status)) } - - val tracedClient = - ClientMiddlewareBuilder.default[IO](MinimalRedactor).build(fakeClient) - val request = Request[IO](Method.GET, uri"http://localhost/?#") - - val status = StatusData(StatusCode.Error, "canceled") - - for { - f <- tracedClient.run(request).surround(IO.canceled).start - _ <- f.joinWithUnit - spans <- testkit.finishedSpans - } yield { - assertEquals(spans.flatMap(_.events.elements), Nil) - assertEquals(spans.map(_.status), List(status)) } - } } } } @@ -368,38 +389,41 @@ class ClientMiddlewareTests extends CatsEffectSuite { TracesTestkit .inMemory[IO]() .use { testkit => - testkit.tracerProvider.get("tracer").flatMap { implicit tracer => - val fakeClient = - Client.fromHttpApp[IO] { - HttpApp[IO](_.body.compile.drain.as(Response[IO](Status.InternalServerError))) - } + implicit val TP: TracerProvider[IO] = testkit.tracerProvider + ClientMiddlewareBuilder + .default[IO](MinimalRedactor) + .build + .flatMap { clientMiddleware => + val fakeClient = + Client.fromHttpApp[IO] { + HttpApp[IO](_.body.compile.drain.as(Response[IO](Status.InternalServerError))) + } + + val tracedClient = clientMiddleware(fakeClient) + val request = Request[IO](Method.GET, uri"http://localhost/") + + val status = StatusData(StatusCode.Error) + + val attributes = Attributes( + Attribute("error.type", "500"), + Attribute("http.request.method", "GET"), + Attribute("http.response.status_code", 500L), + Attribute("network.protocol.version", "1.1"), + Attribute("server.address", "localhost"), + Attribute("server.port", 80L), + Attribute("url.full", "http://localhost/"), + Attribute("url.scheme", "http"), + ) - val tracedClient = - ClientMiddlewareBuilder.default[IO](MinimalRedactor).build(fakeClient) - val request = Request[IO](Method.GET, uri"http://localhost/") - - val status = StatusData(StatusCode.Error) - - val attributes = Attributes( - Attribute("error.type", "500"), - Attribute("http.request.method", "GET"), - Attribute("http.response.status_code", 500L), - Attribute("network.protocol.version", "1.1"), - Attribute("server.address", "localhost"), - Attribute("server.port", 80L), - Attribute("url.full", "http://localhost/"), - Attribute("url.scheme", "http"), - ) - - for { - _ <- tracedClient.run(request).use_ - spans <- testkit.finishedSpans - } yield { - assertEquals(spans.map(_.attributes.elements), List(attributes)) - assertEquals(spans.flatMap(_.events.elements), Nil) - assertEquals(spans.map(_.status), List(status)) + for { + _ <- tracedClient.run(request).use_ + spans <- testkit.finishedSpans + } yield { + assertEquals(spans.map(_.attributes.elements), List(attributes)) + assertEquals(spans.flatMap(_.events.elements), Nil) + assertEquals(spans.map(_.status), List(status)) + } } - } } } } diff --git a/trace/server/src/main/scala/org/http4s/otel4s/middleware/trace/server/ServerMiddlewareBuilder.scala b/trace/server/src/main/scala/org/http4s/otel4s/middleware/trace/server/ServerMiddlewareBuilder.scala index f016201..d98b667 100644 --- a/trace/server/src/main/scala/org/http4s/otel4s/middleware/trace/server/ServerMiddlewareBuilder.scala +++ b/trace/server/src/main/scala/org/http4s/otel4s/middleware/trace/server/ServerMiddlewareBuilder.scala @@ -25,17 +25,18 @@ import cats.effect.kernel.Outcome import cats.effect.syntax.monadCancel._ import cats.syntax.applicative._ import cats.syntax.flatMap._ +import cats.syntax.functor._ import fs2.Stream import org.typelevel.otel4s.KindTransformer import org.typelevel.otel4s.trace.SpanKind import org.typelevel.otel4s.trace.StatusCode -import org.typelevel.otel4s.trace.Tracer +import org.typelevel.otel4s.trace.TracerProvider /** Middleware builder for wrapping an http4s `Server` to add tracing. * * @see [[https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-server]] */ -class ServerMiddlewareBuilder[F[_]: Tracer: MonadCancelThrow] private ( +class ServerMiddlewareBuilder[F[_]: TracerProvider: MonadCancelThrow] private ( redactor: PathAndQueryRedactor, // cannot safely have default value spanDataProvider: SpanDataProvider, routeClassifier: RouteClassifier, @@ -86,11 +87,16 @@ class ServerMiddlewareBuilder[F[_]: Tracer: MonadCancelThrow] private ( */ def buildGenericTracedHttp[G[_]: MonadCancelThrow]( f: Http[G, F] - )(implicit kt: KindTransformer[F, G]): Http[G, F] = - Kleisli { (req: Request[F]) => + )(implicit kt: KindTransformer[F, G]): F[Http[G, F]] = + for { + tracerF <- TracerProvider[F] + .tracer("org.http4s.otel4s.middleware.server") + .withVersion(org.http4s.otel4s.middleware.BuildInfo.version) + .get + } yield Kleisli { (req: Request[F]) => if ( !shouldTrace(req.requestPrelude).shouldTrace || - !Tracer[F].meta.isEnabled + !tracerF.meta.isEnabled ) { f(req) } else { @@ -108,7 +114,7 @@ class ServerMiddlewareBuilder[F[_]: Tracer: MonadCancelThrow] private ( headersAllowedAsAttributes.request, ) MonadCancelThrow[G].uncancelable { poll => - val tracerG = Tracer[F].mapK[G] + val tracerG = tracerF.mapK[G] tracerG.joinOrRoot(req.headers) { tracerG .spanBuilder(spanName) @@ -141,18 +147,18 @@ class ServerMiddlewareBuilder[F[_]: Tracer: MonadCancelThrow] private ( } /** @return a configured middleware for `HttpApp` */ - def buildHttpApp(f: HttpApp[F]): HttpApp[F] = + def buildHttpApp(f: HttpApp[F]): F[HttpApp[F]] = buildGenericTracedHttp(f) /** @return a configured middleware for `HttpRoutes` */ - def buildHttpRoutes(f: HttpRoutes[F]): HttpRoutes[F] = + def buildHttpRoutes(f: HttpRoutes[F]): F[HttpRoutes[F]] = buildGenericTracedHttp(f) } object ServerMiddlewareBuilder { /** @return a server middleware builder with default configuration */ - def default[F[_]: Tracer: MonadCancelThrow]( + def default[F[_]: TracerProvider: MonadCancelThrow]( redactor: PathAndQueryRedactor ): ServerMiddlewareBuilder[F] = new ServerMiddlewareBuilder[F]( diff --git a/trace/server/src/test/scala/org/http4s/otel4s/middleware/trace/server/ServerMiddlewareTests.scala b/trace/server/src/test/scala/org/http4s/otel4s/middleware/trace/server/ServerMiddlewareTests.scala index 885d8e8..3e37864 100644 --- a/trace/server/src/test/scala/org/http4s/otel4s/middleware/trace/server/ServerMiddlewareTests.scala +++ b/trace/server/src/test/scala/org/http4s/otel4s/middleware/trace/server/ServerMiddlewareTests.scala @@ -35,7 +35,7 @@ import org.typelevel.otel4s.sdk.trace.data.LimitedData import org.typelevel.otel4s.sdk.trace.data.StatusData import org.typelevel.otel4s.trace.SpanKind import org.typelevel.otel4s.trace.StatusCode -import org.typelevel.otel4s.trace.Tracer +import org.typelevel.otel4s.trace.TracerProvider import scala.concurrent.duration.Duration import scala.util.control.NoStackTrace @@ -49,24 +49,22 @@ class ServerMiddlewareTests extends CatsEffectSuite { TracesTestkit .inMemory[IO]() .use { testkit => + val headers = Headers(Header.Raw(ci"foo", "bar"), Header.Raw(ci"baz", "qux")) + val response = Response[IO](Status.Ok).withHeaders(headers) for { - tracerIO <- testkit.tracerProvider.get("tracer") - _ <- { - implicit val tracer: Tracer[IO] = tracerIO - val headers = - Headers(Header.Raw(ci"foo", "bar"), Header.Raw(ci"baz", "qux")) - val response = Response[IO](Status.Ok).withHeaders(headers) - val tracedServer = - ServerMiddlewareBuilder - .default[IO](NoopRedactor) - .withHeadersAllowedAsAttributes( - HeadersAllowedAsAttributes( - request = Set(ci"foo"), - response = Set(ci"baz"), - ) + tracedServer <- { + implicit val TP: TracerProvider[IO] = testkit.tracerProvider + ServerMiddlewareBuilder + .default[IO](NoopRedactor) + .withHeadersAllowedAsAttributes( + HeadersAllowedAsAttributes( + request = Set(ci"foo"), + response = Set(ci"baz"), ) - .buildHttpApp(HttpApp[IO](_.body.compile.drain.as(response))) - + ) + .buildHttpApp(HttpApp[IO](_.body.compile.drain.as(response))) + } + _ <- { val request = Request[IO](Method.GET, uri"http://localhost/?#") .withHeaders(headers) @@ -104,44 +102,46 @@ class ServerMiddlewareTests extends CatsEffectSuite { TracesTestkit .inMemory[IO]() .use { testkit => - testkit.tracerProvider.get("tracer").flatMap { implicit tracer => - val error = new RuntimeException("oops") with NoStackTrace {} - - val tracedServer = ServerMiddlewareBuilder - .default[IO](NoopRedactor) - .buildHttpApp(HttpApp[IO](_ => IO.raiseError(error))) + implicit val TP: TracerProvider[IO] = testkit.tracerProvider + val error = new RuntimeException("oops") with NoStackTrace {} + ServerMiddlewareBuilder + .default[IO](NoopRedactor) + .buildHttpApp(HttpApp[IO](_ => IO.raiseError(error))) + .flatMap { implicit tracedServer => + val request = Request[IO](Method.GET, uri"http://localhost/") + + val events = Vector( + EventData.fromException( + Duration.Zero, + error, + LimitedData + .attributes( + spanLimits.maxNumberOfAttributes, + spanLimits.maxAttributeValueLength, + ), + escaped = false, + ) + ) - val request = Request[IO](Method.GET, uri"http://localhost/") + val status = StatusData(StatusCode.Error) - val events = Vector( - EventData.fromException( - Duration.Zero, - error, - LimitedData - .attributes(spanLimits.maxNumberOfAttributes, spanLimits.maxAttributeValueLength), - escaped = false, + val attributes = Attributes( + Attribute("error.type", error.getClass.getName), + Attribute("http.request.method", "GET"), + Attribute("network.protocol.version", "1.1"), + Attribute("url.path", "/"), + Attribute("url.scheme", "http"), ) - ) - - val status = StatusData(StatusCode.Error) - - val attributes = Attributes( - Attribute("error.type", error.getClass.getName), - Attribute("http.request.method", "GET"), - Attribute("network.protocol.version", "1.1"), - Attribute("url.path", "/"), - Attribute("url.scheme", "http"), - ) - - for { - _ <- tracedServer.run(request).attempt - spans <- testkit.finishedSpans - } yield { - assertEquals(spans.map(_.attributes.elements), List(attributes)) - assertEquals(spans.map(_.events.elements), List(events)) - assertEquals(spans.map(_.status), List(status)) + + for { + _ <- tracedServer.run(request).attempt + spans <- testkit.finishedSpans + } yield { + assertEquals(spans.map(_.attributes.elements), List(attributes)) + assertEquals(spans.map(_.events.elements), List(events)) + assertEquals(spans.map(_.status), List(status)) + } } - } } } } @@ -151,31 +151,31 @@ class ServerMiddlewareTests extends CatsEffectSuite { TracesTestkit .inMemory[IO]() .use { testkit => - testkit.tracerProvider.get("tracer").flatMap { implicit tracer => - val tracedServer = ServerMiddlewareBuilder - .default[IO](NoopRedactor) - .buildHttpApp(HttpApp[IO](_ => IO.pure(Response[IO](Status.InternalServerError)))) - - val request = Request[IO](Method.GET, uri"http://localhost/") - val status = StatusData(StatusCode.Error) - - val attributes = Attributes( - Attribute("error.type", "500"), - Attribute("http.request.method", "GET"), - Attribute("http.response.status_code", 500L), - Attribute("network.protocol.version", "1.1"), - Attribute("url.path", "/"), - Attribute("url.scheme", "http"), - ) - - for { - _ <- tracedServer.run(request).attempt - spans <- testkit.finishedSpans - } yield { - assertEquals(spans.map(_.attributes.elements), List(attributes)) - assertEquals(spans.map(_.status), List(status)) + implicit val TP: TracerProvider[IO] = testkit.tracerProvider + ServerMiddlewareBuilder + .default[IO](NoopRedactor) + .buildHttpApp(HttpApp[IO](_ => IO.pure(Response[IO](Status.InternalServerError)))) + .flatMap { implicit tracedServer => + val request = Request[IO](Method.GET, uri"http://localhost/") + val status = StatusData(StatusCode.Error) + + val attributes = Attributes( + Attribute("error.type", "500"), + Attribute("http.request.method", "GET"), + Attribute("http.response.status_code", 500L), + Attribute("network.protocol.version", "1.1"), + Attribute("url.path", "/"), + Attribute("url.scheme", "http"), + ) + + for { + _ <- tracedServer.run(request).attempt + spans <- testkit.finishedSpans + } yield { + assertEquals(spans.map(_.attributes.elements), List(attributes)) + assertEquals(spans.map(_.status), List(status)) + } } - } } } } @@ -185,32 +185,32 @@ class ServerMiddlewareTests extends CatsEffectSuite { TracesTestkit .inMemory[IO]() .use { testkit => - testkit.tracerProvider.get("tracer").flatMap { implicit tracer => - val tracedServer = ServerMiddlewareBuilder - .default[IO](NoopRedactor) - .buildHttpApp(HttpApp[IO](_ => IO.canceled.as(Response[IO](Status.Ok)))) - - val request = Request[IO](Method.GET, uri"http://localhost/") - - val status = StatusData(StatusCode.Error, "canceled") - - val attributes = Attributes( - Attribute("http.request.method", "GET"), - Attribute("network.protocol.version", "1.1"), - Attribute("url.path", "/"), - Attribute("url.scheme", "http"), - ) - - for { - f <- tracedServer.run(request).void.start - _ <- f.joinWithUnit - spans <- testkit.finishedSpans - } yield { - assertEquals(spans.map(_.attributes.elements), List(attributes)) - assertEquals(spans.flatMap(_.events.elements), Nil) - assertEquals(spans.map(_.status), List(status)) + implicit val TP: TracerProvider[IO] = testkit.tracerProvider + ServerMiddlewareBuilder + .default[IO](NoopRedactor) + .buildHttpApp(HttpApp[IO](_ => IO.canceled.as(Response[IO](Status.Ok)))) + .flatMap { implicit tracedServer => + val request = Request[IO](Method.GET, uri"http://localhost/") + + val status = StatusData(StatusCode.Error, "canceled") + + val attributes = Attributes( + Attribute("http.request.method", "GET"), + Attribute("network.protocol.version", "1.1"), + Attribute("url.path", "/"), + Attribute("url.scheme", "http"), + ) + + for { + f <- tracedServer.run(request).void.start + _ <- f.joinWithUnit + spans <- testkit.finishedSpans + } yield { + assertEquals(spans.map(_.attributes.elements), List(attributes)) + assertEquals(spans.flatMap(_.events.elements), Nil) + assertEquals(spans.map(_.status), List(status)) + } } - } } } }