Skip to content

Commit

Permalink
Merge pull request #678 from ptrdom/item-667
Browse files Browse the repository at this point in the history
Instrument `http4s`
  • Loading branch information
lgajowy authored Apr 27, 2023
2 parents af50426 + 1bbbb86 commit f772173
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 1 deletion.
4 changes: 3 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,16 @@ 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") ++
openTelemetryInstrumentationApiSemanticConventions ++
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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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<TypeInstrumentation> typeInstrumentations() {
return Collections.singletonList(Http4sEmberServerInstrumentations.serverHelpersRunApp());
}

@Override
public List<String> getAdditionalHelperClassNames() {
return List.of(
"io.scalac.mesmer.otelextension.instrumentations.http4s.ember.server.advice.ServerHelpersRunAppAdviceHelper$"
);
}

@Override
public Map<String, ClassRef> getMuzzleReferences() {
return Collections.emptyMap();
}

@Override
public void registerMuzzleVirtualFields(VirtualFieldMappingsBuilder builder) {}

@Override
public List<String> getMuzzleHelperClassNames() {
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.scalac.mesmer.otelextension.instrumentations.http4s.ember.server

import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation

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

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"
)
)
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.scalac.mesmer.otelextension.instrumentations.http4s.ember.server.advice

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")

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)
.attemptTap { response =>
IO.fromTry(Try {
val allAttributes = requestAttributes.toBuilder
.putAll(
response
.map(attributesForResponse(_).build())
.getOrElse(Attributes.empty())
)
.build()

requestsTotal.add(1, allAttributes)

requestDuration.record((System.nanoTime() - startTime) / 1e9d, allAttributes)

concurrentRequests.add(-1, requestAttributes)
})
}
}
}
10 changes: 10 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit f772173

Please sign in to comment.