From c41c17ff3ff8f3ef70a3f2a40f49b3ce6668fbd2 Mon Sep 17 00:00:00 2001 From: Domantas Petrauskas Date: Fri, 7 Apr 2023 13:58:48 +0300 Subject: [PATCH 1/3] Add http4s ember server metrics --- build.sbt | 4 +- .../ember/server/Http4sEmberServerTest.scala | 142 ++++++++++++++++++ ...ttp4sEmberServerInstrumentationModule.java | 46 ++++++ .../Http4sEmberServerInstrumentations.scala | 20 +++ .../advice/ServerHelpersRunAppAdvice.java | 12 ++ .../ServerHelpersRunAppAdviceHelper.scala | 58 +++++++ project/Dependencies.scala | 10 ++ 7 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 otel-extension/src/it/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/Http4sEmberServerTest.scala create mode 100644 otel-extension/src/main/java/io/scalac/mesmer/otelextension/http4s/ember/server/MesmerHttp4sEmberServerInstrumentationModule.java create mode 100644 otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/Http4sEmberServerInstrumentations.scala create mode 100644 otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdvice.java create mode 100644 otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdviceHelper.scala diff --git a/build.sbt b/build.sbt index 44fb0f29..d87c19bc 100644 --- a/build.sbt +++ b/build.sbt @@ -76,6 +76,7 @@ lazy val otelExtension = (project in file("otel-extension")) excludeDependencies += "io.opentelemetry.javaagent" % "opentelemetry-javaagent-bootstrap", libraryDependencies ++= { zio.map(_ % "provided") ++ + http4s.map(_ % "provided") ++ openTelemetryExtension.map(_ % "provided") ++ opentelemetryExtensionApi ++ openTelemetryMuzzle.map(_ % "provided") ++ @@ -83,7 +84,8 @@ lazy val otelExtension = (project in file("otel-extension")) byteBuddy.map(_ % "provided") ++ akkaTestkit.map(_ % "it,test") ++ scalatest.map(_ % "it,test") ++ - openTelemetryTesting.map(_ % "it,test") + openTelemetryTesting.map(_ % "it,test") ++ + http4sClient.map(_ % "it,test") }, assembly / test := {}, assembly / assemblyJarName := s"${name.value}-assembly.jar", diff --git a/otel-extension/src/it/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/Http4sEmberServerTest.scala b/otel-extension/src/it/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/Http4sEmberServerTest.scala new file mode 100644 index 00000000..84a7d821 --- /dev/null +++ b/otel-extension/src/it/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/Http4sEmberServerTest.scala @@ -0,0 +1,142 @@ +package io.scalac.mesmer.otelextension.instrumentations.http4s.ember.server + +import cats.effect._ +import cats.effect.unsafe.implicits.global +import cats.syntax.all._ +import com.comcast.ip4s._ +import io.opentelemetry.api.common.Attributes +import io.scalac.mesmer.agent.utils.OtelAgentTest +import io.scalac.mesmer.core.config.MesmerPatienceConfig +import org.http4s.HttpApp +import org.http4s.HttpRoutes +import org.http4s.Uri +import org.http4s.ember.client.EmberClientBuilder +import org.http4s.ember.server.EmberServerBuilder +import org.scalatest.BeforeAndAfterEach +import org.scalatest.Inside +import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.matchers.should.Matchers + +import scala.jdk.CollectionConverters._ + +class Http4sEmberServerTest + extends AnyFreeSpec + with OtelAgentTest + with Matchers + with MesmerPatienceConfig + with BeforeAndAfterEach + with Inside { + import Http4sEmberServerTest._ + + private def service(block: () => Unit): HttpApp[IO] = { + import org.http4s.dsl.io._ + + HttpRoutes + .of[IO] { case GET -> Root => + block() + Ok("") + } + .orNotFound + } + + private def url(address: SocketAddress[Host], path: String = ""): Uri = + Uri.unsafeFromString( + s"http://${Uri.Host.fromIp4sHost(address.host).renderString}:${address.port.value}$path" + ) + + private def server(block: () => Unit) = EmberServerBuilder + .default[IO] + .withHttpApp(service(block)) + .withPort(port"0") + .build + + private val client = EmberClientBuilder.default[IO].build + + private def doGetRootCall(block: () => Unit = () => ()) = { + server(block) + .use(server => + client.use(client => + client + .get(url(server.addressIp4s))(_.status.pure[IO]) + ) + ) + .unsafeRunSync() + () + } + + "http4s ember server" - { + "should record" - { + "requests" - { + "total counter" in { + doGetRootCall() + + assertMetric("mesmer_http4s_ember_server_requests") { data => + inside(data.getLongSumData.getPoints.asScala.toList) { case List(point) => + point.getValue shouldEqual 1 + point.getAttributes.asScalaMap() should contain theSameElementsAs Map( + "method" -> "GET", + "path" -> "/", + "status" -> "200" + ) + } + } + } + + "duration histogram" in { + doGetRootCall() + + assertMetric("mesmer_http4s_ember_server_request_duration_seconds") { data => + inside(data.getHistogramData.getPoints.asScala.toList) { case List(point) => + point.getAttributes.asScalaMap() should contain theSameElementsAs Map( + "method" -> "GET", + "path" -> "/", + "status" -> "200" + ) + } + } + } + + "concurrent counter" in { + val expectedAttributes = Map( + "method" -> "GET", + "path" -> "/" + ) + + val assertZeroConcurrentRequests = () => + assertMetric("mesmer_http4s_ember_server_concurrent_requests") { data => + inside(data.getLongSumData.getPoints.asScala.toList) { case List(point) => + point.getValue shouldEqual 0 + point.getAttributes.asScalaMap() should contain theSameElementsAs expectedAttributes + } + } + + assertZeroConcurrentRequests() + + doGetRootCall { () => + assertMetric("mesmer_http4s_ember_server_concurrent_requests") { data => + inside(data.getLongSumData.getPoints.asScala.toList) { case List(point) => + point.getValue shouldEqual 1 + point.getAttributes.asScalaMap() should contain theSameElementsAs expectedAttributes + } + } + } + + assertZeroConcurrentRequests() + } + } + } + } +} + +object Http4sEmberServerTest { + implicit class AttributesAsMap(attributes: Attributes) { + def asScalaMap(): Map[String, AnyRef] = + attributes + .asMap() + .asScala + .map { case (k, v) => + (k.getKey, v) + } + .toMap + } +} diff --git a/otel-extension/src/main/java/io/scalac/mesmer/otelextension/http4s/ember/server/MesmerHttp4sEmberServerInstrumentationModule.java b/otel-extension/src/main/java/io/scalac/mesmer/otelextension/http4s/ember/server/MesmerHttp4sEmberServerInstrumentationModule.java new file mode 100644 index 00000000..5a170f43 --- /dev/null +++ b/otel-extension/src/main/java/io/scalac/mesmer/otelextension/http4s/ember/server/MesmerHttp4sEmberServerInstrumentationModule.java @@ -0,0 +1,46 @@ +package io.scalac.mesmer.otelextension.http4s.ember.server; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.tooling.muzzle.InstrumentationModuleMuzzle; +import io.opentelemetry.javaagent.tooling.muzzle.VirtualFieldMappingsBuilder; +import io.opentelemetry.javaagent.tooling.muzzle.references.ClassRef; +import io.scalac.mesmer.otelextension.instrumentations.http4s.ember.server.Http4sEmberServerInstrumentations; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@AutoService(InstrumentationModule.class) +public class MesmerHttp4sEmberServerInstrumentationModule extends InstrumentationModule + implements InstrumentationModuleMuzzle { + public MesmerHttp4sEmberServerInstrumentationModule() { + super("mesmer-http4s-ember-server"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(Http4sEmberServerInstrumentations.serverHelpersRunApp()); + } + + @Override + public List getAdditionalHelperClassNames() { + return List.of( + "io.scalac.mesmer.otelextension.instrumentations.http4s.ember.server.advice.ServerHelpersRunAppAdviceHelper$" + ); + } + + @Override + public Map getMuzzleReferences() { + return Collections.emptyMap(); + } + + @Override + public void registerMuzzleVirtualFields(VirtualFieldMappingsBuilder builder) {} + + @Override + public List getMuzzleHelperClassNames() { + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/Http4sEmberServerInstrumentations.scala b/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/Http4sEmberServerInstrumentations.scala new file mode 100644 index 00000000..31c21d7c --- /dev/null +++ b/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/Http4sEmberServerInstrumentations.scala @@ -0,0 +1,20 @@ +package io.scalac.mesmer.otelextension.instrumentations.http4s.ember.server + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation +import io.scalac.mesmer.agent.util.dsl.matchers.isConstructor +import io.scalac.mesmer.agent.util.dsl.matchers.named +import io.scalac.mesmer.agent.util.i13n.Advice +import io.scalac.mesmer.agent.util.i13n.Instrumentation +import net.bytebuddy.description.method.MethodDescription + +object Http4sEmberServerInstrumentations { + + val serverHelpersRunApp: TypeInstrumentation = + Instrumentation(named("org.http4s.ember.server.internal.ServerHelpers$")) + .`with`( + Advice( + named("runApp"), + "io.scalac.mesmer.otelextension.instrumentations.http4s.ember.server.advice.ServerHelpersRunAppAdvice" + ) + ) +} diff --git a/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdvice.java b/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdvice.java new file mode 100644 index 00000000..27b2e57f --- /dev/null +++ b/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdvice.java @@ -0,0 +1,12 @@ +package io.scalac.mesmer.otelextension.instrumentations.http4s.ember.server.advice; + +import cats.data.Kleisli; +import net.bytebuddy.asm.Advice; + +public class ServerHelpersRunAppAdvice { + + @Advice.OnMethodEnter + public static void runAppEnter(@Advice.Argument(value = 4, readOnly = false) Kleisli httpApp) { + httpApp = ServerHelpersRunAppAdviceHelper.withMetrics(httpApp); + } +} diff --git a/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdviceHelper.scala b/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdviceHelper.scala new file mode 100644 index 00000000..91519b87 --- /dev/null +++ b/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdviceHelper.scala @@ -0,0 +1,58 @@ +package io.scalac.mesmer.otelextension.instrumentations.http4s.ember.server.advice + +import cats.data.Kleisli +import cats.effect.IO +import cats.effect.kernel.Outcome +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.common.Attributes +import org.http4s.ContextRequest +import org.http4s.HttpApp +import org.http4s.Request +import org.http4s.Response +import org.http4s.server.middleware.BracketRequestResponse + +object ServerHelpersRunAppAdviceHelper { + + private val meter = GlobalOpenTelemetry.getMeter("mesmer") + + private val requestsTotal = meter + .counterBuilder("mesmer_http4s_ember_server_requests") + .build() + + private val concurrentRequests = meter + .upDownCounterBuilder("mesmer_http4s_ember_server_concurrent_requests") + .build() + + private val requestDuration = meter + .histogramBuilder("mesmer_http4s_ember_server_request_duration_seconds") + .build() + + private def attributesForRequest(request: Request[IO]) = + Attributes.builder().put("method", request.method.name).put("path", request.pathInfo.renderString) + + private def attributesForResponse(response: Response[IO]) = + Attributes.builder().put("status", response.status.code.toString) + + def withMetrics(httpApp: Any): Kleisli[IO, Request[IO], Response[IO]] = + Kleisli[IO, Request[IO], Response[IO]] { request => + val requestAttributes = attributesForRequest(request).build() + val startTime = System.nanoTime() + + concurrentRequests.add(1, requestAttributes) + + httpApp + .asInstanceOf[HttpApp[IO]] + .run(request) + .map { response => + val allAttributes = requestAttributes.toBuilder.putAll(attributesForResponse(response).build()).build() + + requestsTotal.add(1, allAttributes) + + requestDuration.record((System.nanoTime() - startTime) / 1e9d, allAttributes) + + concurrentRequests.add(-1, requestAttributes) + + response + } + } +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index bcd46734..979092e2 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -9,6 +9,7 @@ object Dependencies { val ByteBuddyVersion = "1.14.2" val CirceVersion = "0.14.5" val CirceYamlVersion = "0.14.2" + val Http4sVersion = "0.23.18" val GoogleAutoServiceVersion = "1.0.1" val LogbackVersion = "1.4.6" @@ -43,6 +44,15 @@ object Dependencies { "dev.zio" %% "zio" % "2.0.10" ) + val http4s = Seq( + "org.http4s" %% "http4s-ember-server" % Http4sVersion, + "org.http4s" %% "http4s-dsl" % Http4sVersion + ) + + val http4sClient = Seq( + "org.http4s" %% "http4s-ember-client" % Http4sVersion + ) + val byteBuddy = Seq( "net.bytebuddy" % "byte-buddy" % ByteBuddyVersion, "net.bytebuddy" % "byte-buddy-agent" % ByteBuddyVersion From 642fab4971512c8aa6f75c6322c36f7681741f87 Mon Sep 17 00:00:00 2001 From: Domantas Petrauskas Date: Wed, 12 Apr 2023 11:06:17 +0300 Subject: [PATCH 2/3] Add http4s concurrent requests metric --- .../ember/server/Http4sEmberServerInstrumentations.scala | 3 +-- .../ember/server/advice/ServerHelpersRunAppAdviceHelper.scala | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/Http4sEmberServerInstrumentations.scala b/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/Http4sEmberServerInstrumentations.scala index 31c21d7c..d2b8b310 100644 --- a/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/Http4sEmberServerInstrumentations.scala +++ b/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/Http4sEmberServerInstrumentations.scala @@ -1,11 +1,10 @@ package io.scalac.mesmer.otelextension.instrumentations.http4s.ember.server import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation -import io.scalac.mesmer.agent.util.dsl.matchers.isConstructor + import io.scalac.mesmer.agent.util.dsl.matchers.named import io.scalac.mesmer.agent.util.i13n.Advice import io.scalac.mesmer.agent.util.i13n.Instrumentation -import net.bytebuddy.description.method.MethodDescription object Http4sEmberServerInstrumentations { diff --git a/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdviceHelper.scala b/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdviceHelper.scala index 91519b87..7a3fe5e5 100644 --- a/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdviceHelper.scala +++ b/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdviceHelper.scala @@ -2,14 +2,11 @@ package io.scalac.mesmer.otelextension.instrumentations.http4s.ember.server.advi import cats.data.Kleisli import cats.effect.IO -import cats.effect.kernel.Outcome import io.opentelemetry.api.GlobalOpenTelemetry import io.opentelemetry.api.common.Attributes -import org.http4s.ContextRequest import org.http4s.HttpApp import org.http4s.Request import org.http4s.Response -import org.http4s.server.middleware.BracketRequestResponse object ServerHelpersRunAppAdviceHelper { From 1bbbb8687ab7518690736a2ac17c240c1de31bea Mon Sep 17 00:00:00 2001 From: Domantas Petrauskas Date: Wed, 12 Apr 2023 16:21:27 +0300 Subject: [PATCH 3/3] Handle http4s ember server metrics in error cases --- .../ServerHelpersRunAppAdviceHelper.scala | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdviceHelper.scala b/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdviceHelper.scala index 7a3fe5e5..028bdfb9 100644 --- a/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdviceHelper.scala +++ b/otel-extension/src/main/scala/io/scalac/mesmer/otelextension/instrumentations/http4s/ember/server/advice/ServerHelpersRunAppAdviceHelper.scala @@ -2,12 +2,15 @@ package io.scalac.mesmer.otelextension.instrumentations.http4s.ember.server.advi import cats.data.Kleisli import cats.effect.IO +import cats.implicits._ import io.opentelemetry.api.GlobalOpenTelemetry import io.opentelemetry.api.common.Attributes import org.http4s.HttpApp import org.http4s.Request import org.http4s.Response +import scala.util.Try + object ServerHelpersRunAppAdviceHelper { private val meter = GlobalOpenTelemetry.getMeter("mesmer") @@ -40,16 +43,22 @@ object ServerHelpersRunAppAdviceHelper { httpApp .asInstanceOf[HttpApp[IO]] .run(request) - .map { response => - val allAttributes = requestAttributes.toBuilder.putAll(attributesForResponse(response).build()).build() - - requestsTotal.add(1, allAttributes) + .attemptTap { response => + IO.fromTry(Try { + val allAttributes = requestAttributes.toBuilder + .putAll( + response + .map(attributesForResponse(_).build()) + .getOrElse(Attributes.empty()) + ) + .build() - requestDuration.record((System.nanoTime() - startTime) / 1e9d, allAttributes) + requestsTotal.add(1, allAttributes) - concurrentRequests.add(-1, requestAttributes) + requestDuration.record((System.nanoTime() - startTime) / 1e9d, allAttributes) - response + concurrentRequests.add(-1, requestAttributes) + }) } } }