From 904b670225dc49857d391e9afb0b90ccd18907bb Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Thu, 12 Sep 2024 20:32:49 +0300 Subject: [PATCH] Use `otel4s-semconv-metrics-experimental` for semantic testing --- build.sbt | 3 +- .../middleware/metrics/OtelMetrics.scala | 2 +- .../middleware/metrics/OtelMetricsTests.scala | 84 ++++++++++++++++++- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index 18a0154..0ce7518 100644 --- a/build.sbt +++ b/build.sbt @@ -67,11 +67,12 @@ lazy val metrics = crossProject(JVMPlatform, JSPlatform, NativePlatform) .settings( name := s"$baseName-metrics", libraryDependencies ++= Seq( - "org.http4s" %%% "http4s-core" % http4sV, + "org.http4s" %%% "http4s-client" % http4sV, "org.typelevel" %%% "otel4s-core-common" % otel4sV, "org.typelevel" %%% "otel4s-core-metrics" % otel4sV, "org.typelevel" %%% "otel4s-semconv" % otel4sV, "org.http4s" %%% "http4s-server" % http4sV % Test, + "org.typelevel" %%% "otel4s-semconv-metrics-experimental" % otel4sV % Test, ), ) 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..f4a30b0 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 @@ -179,7 +179,7 @@ object OtelMetrics { Meter[F] .upDownCounter[Long](s"http.$kind.active_requests") .withUnit("{request}") - .withDescription("Number of active HTTP requests.") + .withDescription(s"Number of active HTTP $kind requests.") .create val abnormalTerminations: F[Histogram[F, Double]] = 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..8c0d411 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 @@ -20,12 +20,19 @@ package otel4s.middleware.metrics import cats.data.OptionT import cats.effect.IO import munit.CatsEffectSuite -import org.http4s.server.middleware.Metrics +import org.http4s.client.Client +import org.http4s.client.middleware.{Metrics => ClientMetrics} +import org.http4s.server.middleware.{Metrics => ServerMetrics} import org.typelevel.otel4s.Attributes import org.typelevel.otel4s.metrics.Meter +import org.typelevel.otel4s.sdk.metrics.data.MetricData import org.typelevel.otel4s.sdk.metrics.data.MetricPoints import org.typelevel.otel4s.sdk.metrics.data.PointData import org.typelevel.otel4s.sdk.testkit.metrics.MetricsTestkit +import org.typelevel.otel4s.semconv.MetricSpec +import org.typelevel.otel4s.semconv.Requirement +import org.typelevel.otel4s.semconv.experimental.metrics.HttpExperimentalMetrics +import org.typelevel.otel4s.semconv.metrics.HttpMetrics class OtelMetricsTests extends CatsEffectSuite { test("OtelMetrics") { @@ -41,7 +48,7 @@ class OtelMetricsTests extends CatsEffectSuite { _ <- { val fakeServer = HttpRoutes[IO](e => OptionT.liftF(e.body.compile.drain.as(Response[IO](Status.Ok)))) - val meteredServer = Metrics[IO](metricsOps)(fakeServer) + val meteredServer = ServerMetrics[IO](metricsOps)(fakeServer) meteredServer .run(Request[IO](Method.GET)) @@ -101,4 +108,77 @@ class OtelMetricsTests extends CatsEffectSuite { } } } + + test("server semantic test") { + val specs = List( + HttpMetrics.ServerRequestDuration, + HttpExperimentalMetrics.ServerActiveRequests, + ) + + MetricsTestkit.inMemory[IO]().use { testkit => + testkit.meterProvider.get("meter").flatMap { implicit meter => + val fakeServer = + HttpRoutes[IO](e => OptionT.liftF(e.body.compile.drain.as(Response[IO](Status.Ok)))) + + for { + metricsOps <- OtelMetrics.serverMetricsOps[IO]() + server = ServerMetrics[IO](metricsOps)(fakeServer) + + _ <- server.run(Request[IO](Method.GET)).semiflatMap(_.body.compile.drain).value + metrics <- testkit.collectMetrics + } yield specs.foreach(spec => specTest(metrics, spec)) + } + } + } + + test("client semantic test") { + val specs = List( + HttpMetrics.ClientRequestDuration, + HttpExperimentalMetrics.ClientActiveRequests, + ) + + MetricsTestkit.inMemory[IO]().use { testkit => + testkit.meterProvider.get("meter").flatMap { implicit meter => + val fakeServer = + HttpRoutes[IO](e => OptionT.liftF(e.body.compile.drain.as(Response[IO](Status.Ok)))) + + for { + clientOps <- OtelMetrics.clientMetricsOps[IO]() + client = ClientMetrics[IO](clientOps)(Client.fromHttpApp(fakeServer.orNotFound)) + + _ <- client.run(Request[IO](Method.GET)).use(_.body.compile.drain) + metrics <- testkit.collectMetrics + } yield specs.foreach(spec => specTest(metrics, spec)) + } + } + } + + private def specTest(metrics: List[MetricData], spec: MetricSpec): Unit = { + val metric = metrics.find(_.name == spec.name) + assert( + metric.isDefined, + s"${spec.name} metric is missing. Available [${metrics.map(_.name).mkString(", ")}]", + ) + + val clue = s"[${spec.name}] has a mismatched property" + + metric.foreach { md => + assertEquals(md.name, spec.name, clue) + assertEquals(md.description, Some(spec.description), clue) + assertEquals(md.unit, Some(spec.unit), clue) + + val required = spec.attributeSpecs + .filter(_.requirement.level == Requirement.Level.Required) + .map(_.key) + .toSet + + val current = md.data.points.toVector + .flatMap(_.attributes.map(_.key)) + .filter(key => required.contains(key)) + .toSet + + assertEquals(current, required, clue) + } + } + }