Repository: open-telemetry/opentelemetry-java-instrumentation
Pull request #13386: Add tapir path matching within pekko instrumentation
Author: login: masonedmison
Created: 2025-02-24T16:46:47Z | Merged: 2025-03-07T23:18:14Z

- [x] #13360 must be merged first.


Diff:
diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/build.gradle.kts b/instrumentation/pekko/pekko-http-1.0/javaagent/build.gradle.kts
index 9567d8641157..7c672ff8955f 100644
--- a/instrumentation/pekko/pekko-http-1.0/javaagent/build.gradle.kts
+++ b/instrumentation/pekko/pekko-http-1.0/javaagent/build.gradle.kts
@@ -10,6 +10,7 @@ muzzle {
     versions.set("[1.0,)")
     assertInverse.set(true)
     extraDependency("org.apache.pekko:pekko-stream_2.12:1.0.1")
+    excludeInstrumentationName("tapir-pekko-http-server")
   }
   pass {
     group.set("org.apache.pekko")
@@ -17,6 +18,7 @@ muzzle {
     versions.set("[1.0,)")
     assertInverse.set(true)
     extraDependency("org.apache.pekko:pekko-stream_2.13:1.0.1")
+    excludeInstrumentationName("tapir-pekko-http-server")
   }
   pass {
     group.set("org.apache.pekko")
@@ -24,21 +26,63 @@ muzzle {
     versions.set("[1.0,)")
     assertInverse.set(true)
     extraDependency("org.apache.pekko:pekko-stream_3:1.0.1")
+    excludeInstrumentationName("tapir-pekko-http-server")
+  }
+  pass {
+    group.set("com.softwaremill.sttp.tapir")
+    module.set("tapir-pekko-http-server_2.12")
+    versions.set("[1.7,)")
+    assertInverse.set(true)
+    excludeInstrumentationName("pekko-http-server")
+  }
+  pass {
+    group.set("com.softwaremill.sttp.tapir")
+    module.set("tapir-pekko-http-server_2.13")
+    versions.set("[1.7,)")
+    assertInverse.set(true)
+    excludeInstrumentationName("pekko-http-server")
+  }
+  pass {
+    group.set("com.softwaremill.sttp.tapir")
+    module.set("tapir-pekko-http-server_3")
+    versions.set("[1.7,)")
+    assertInverse.set(true)
+    excludeInstrumentationName("pekko-http-server")
   }
 }
 
 dependencies {
   library("org.apache.pekko:pekko-http_2.12:1.0.0")
   library("org.apache.pekko:pekko-stream_2.12:1.0.1")
-
-  testImplementation("com.softwaremill.sttp.tapir:tapir-pekko-http-server_2.12:1.7.0")
+  compileOnly("com.softwaremill.sttp.tapir:tapir-pekko-http-server_2.12:1.7.0")
 
   testInstrumentation(project(":instrumentation:pekko:pekko-actor-1.0:javaagent"))
   testInstrumentation(project(":instrumentation:executors:javaagent"))
 
   latestDepTestLibrary("org.apache.pekko:pekko-http_2.13:latest.release")
   latestDepTestLibrary("org.apache.pekko:pekko-stream_2.13:latest.release")
-  latestDepTestLibrary("com.softwaremill.sttp.tapir:tapir-pekko-http-server_2.13:latest.release")
+}
+
+testing {
+  suites {
+    val tapirTest by registering(JvmTestSuite::class) {
+      dependencies {
+        // this only exists to make Intellij happy since it doesn't (currently at least) understand our
+        // inclusion of this artifact inside :testing-common
+        compileOnly(project.dependencies.project(":testing:armeria-shaded-for-testing", configuration = "shadow"))
+
+        if (findProperty("testLatestDeps") as Boolean) {
+          implementation("com.typesafe.akka:akka-http_2.13:latest.release")
+          implementation("com.typesafe.akka:akka-stream_2.13:latest.release")
+          implementation("com.softwaremill.sttp.tapir:tapir-pekko-http-server_2.13:latest.release")
+        } else {
+          implementation("org.apache.pekko:pekko-http_2.12:1.0.0")
+          implementation("org.apache.pekko:pekko-stream_2.12:1.0.1")
+          implementation("com.softwaremill.sttp.tapir:tapir-pekko-http-server_2.12:1.7.0")
+        }
+      }
+    }
+  }
 }
 
 tasks {
@@ -51,6 +95,10 @@ tasks {
 
     systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean)
   }
+
+  check {
+    dependsOn(testing.suites)
+  }
 }
 
 if (findProperty("testLatestDeps") as Boolean) {
@@ -59,7 +107,6 @@ if (findProperty("testLatestDeps") as Boolean) {
     testImplementation {
       exclude("org.apache.pekko", "pekko-http_2.12")
       exclude("org.apache.pekko", "pekko-stream_2.12")
-      exclude("com.softwaremill.sttp.tapir", "tapir-pekko-http-server_2.12")
     }
   }
 }
diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/tapir/RouteWrapper.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/tapir/RouteWrapper.java
new file mode 100644
index 000000000000..8f9237e89023
--- /dev/null
+++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/tapir/RouteWrapper.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.tapir;
+
+import io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route.PekkoRouteHolder;
+import org.apache.pekko.http.scaladsl.server.RequestContext;
+import org.apache.pekko.http.scaladsl.server.RouteResult;
+import scala.Function1;
+import scala.Function2;
+import scala.Option;
+import scala.PartialFunction;
+import scala.Unit;
+import scala.concurrent.Future;
+import scala.util.Try;
+import sttp.tapir.EndpointInput;
+import sttp.tapir.server.ServerEndpoint;
+
+public class RouteWrapper implements Function1<RequestContext, Future<RouteResult>> {
+  private final Function1<RequestContext, Future<RouteResult>> route;
+  private final ServerEndpoint<?, ?> serverEndpoint;
+
+  public RouteWrapper(
+      ServerEndpoint<?, ?> serverEndpoint, Function1<RequestContext, Future<RouteResult>> route) {
+    this.route = route;
+    this.serverEndpoint = serverEndpoint;
+  }
+
+  public class Finalizer implements PartialFunction<Try<RouteResult>, Unit> {
+    @Override
+    public boolean isDefinedAt(Try<RouteResult> tryResult) {
+      return tryResult.isSuccess();
+    }
+
+    @Override
+    public Unit apply(Try<RouteResult> tryResult) {
+      if (tryResult.isSuccess()) {
+        RouteResult result = tryResult.get();
+        if (result.getClass() == RouteResult.Complete.class) {
+          String path =
+              serverEndpoint.showPathTemplate(
+                  (index, pc) ->
+                      pc.name().isDefined() ? "{" + pc.name().get() + "}" : "{param" + index + "}",
+                  Option.apply(
+                      (Function2<Object, EndpointInput.Query<?>, String>)
+                          (index, q) -> q.name() + "={" + q.name() + "}"),
+                  false,
+                  "*",
+                  Option.apply("*"),
+                  Option.apply("*"));
+
+          PekkoRouteHolder.push(path);
+          PekkoRouteHolder.endMatched();
+        }
+      }
+      return null;
+    }
+  }
+
+  @Override
+  public Future<RouteResult> apply(RequestContext ctx) {
+    return route.apply(ctx).andThen(new Finalizer(), ctx.executionContext());
+  }
+}
diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/tapir/TapirPathInstrumentation.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/tapir/TapirPathInstrumentation.java
new file mode 100644
index 000000000000..9e7a6d50fffc
--- /dev/null
+++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/tapir/TapirPathInstrumentation.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.tapir;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import org.apache.pekko.http.scaladsl.server.RequestContext;
+import org.apache.pekko.http.scaladsl.server.RouteResult;
+import scala.Function1;
+import scala.concurrent.Future;
+import sttp.tapir.server.ServerEndpoint;
+
+public class TapirPathInstrumentation implements TypeInstrumentation {
+
+  @Override
+  public ElementMatcher<TypeDescription> typeMatcher() {
+    return named("sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter");
+  }
+
+  @Override
+  public void transform(TypeTransformer transformer) {
+    transformer.applyAdviceToMethod(
+        named("toRoute").and(takesArgument(0, named("sttp.tapir.server.ServerEndpoint"))),
+        this.getClass().getName() + "$ApplyAdvice");
+  }
+
+  @SuppressWarnings("unused")
+  public static class ApplyAdvice {
+    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+    public static void onExit(
+        @Advice.Argument(0) ServerEndpoint<?, ?> endpoint,
+        @Advice.Return(readOnly = false) Function1<RequestContext, Future<RouteResult>> route) {
+      route = new RouteWrapper(endpoint, route);
+    }
+  }
+}
diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/tapir/TapirPekkoHttpServerRouteInstrumentationModule.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/tapir/TapirPekkoHttpServerRouteInstrumentationModule.java
new file mode 100644
index 000000000000..8998e62b3191
--- /dev/null
+++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/tapir/TapirPekkoHttpServerRouteInstrumentationModule.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.tapir;
+
+import static java.util.Collections.singletonList;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule;
+import java.util.List;
+
+@AutoService(InstrumentationModule.class)
+public class TapirPekkoHttpServerRouteInstrumentationModule extends InstrumentationModule
+    implements ExperimentalInstrumentationModule {
+  public TapirPekkoHttpServerRouteInstrumentationModule() {
+    super(
+        "pekko-http",
+        "pekko-http-1.0",
+        "pekko-http-server",
+        "pekko-http-server-route",
+        "tapir-pekko-http-server",
+        "tapir-pekko-http-server-route");
+  }
+
+  @Override
+  public String getModuleGroup() {
+    return "pekko-server";
+  }
+
+  @Override
+  public List<TypeInstrumentation> typeInstrumentations() {
+    return singletonList(new TapirPathInstrumentation());
+  }
+}
diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/tapirTest/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/TapirHttpServerRouteTest.scala b/instrumentation/pekko/pekko-http-1.0/javaagent/src/tapirTest/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/TapirHttpServerRouteTest.scala
new file mode 100644
index 000000000000..e7dc6d33f8e5
--- /dev/null
+++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/tapirTest/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/TapirHttpServerRouteTest.scala
@@ -0,0 +1,133 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0
+
+import io.opentelemetry.instrumentation.test.utils.PortUtils
+import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension
+import io.opentelemetry.sdk.testing.assertj.{SpanDataAssert, TraceAssert}
+import io.opentelemetry.testing.internal.armeria.client.WebClient
+import io.opentelemetry.testing.internal.armeria.common.{
+  AggregatedHttpRequest,
+  HttpMethod
+}
+import org.apache.pekko.actor.ActorSystem
+import org.apache.pekko.http.scaladsl.Http
+import org.apache.pekko.http.scaladsl.server.Directives.{
+  IntNumber,
+  complete,
+  concat,
+  path,
+  pathEndOrSingleSlash,
+  pathPrefix,
+  pathSingleSlash
+}
+import org.apache.pekko.http.scaladsl.server.Route
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.extension.RegisterExtension
+import org.junit.jupiter.api.{AfterAll, Test, TestInstance}
+import sttp.tapir._
+import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter
+
+import java.net.{URI, URISyntaxException}
+import java.util.function.Consumer
+import scala.concurrent.duration.DurationInt
+import scala.concurrent.{Await, Future}
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class TapirHttpServerRouteTest {
+  @RegisterExtension private val testing: AgentInstrumentationExtension =
+    AgentInstrumentationExtension.create
+  private val client: WebClient = WebClient.of()
+
+  implicit val system: ActorSystem = ActorSystem("my-system")
+
+  private def buildAddress(port: Int): URI = try
+    new URI("http://localhost:" + port + "/")
+  catch {
+    case exception: URISyntaxException =>
+      throw new IllegalStateException(exception)
+  }
+
+  @Test def testSimple(): Unit = {
+    val route = path("test") {
+      complete("ok")
+    }
+
+    test(route, "/test", "GET /test")
+  }
+
+  @Test def testRoute(): Unit = {
+    val route = concat(
+      pathEndOrSingleSlash {
+        complete("root")
+      },
+      pathPrefix("test") {
+        concat(
+          pathSingleSlash {
+            complete("test")
+          },
+          path(IntNumber) { _ =>
+            complete("ok")
+          }
+        )
+      }
+    )
+
+    test(route, "/test/1", "GET /test/*")
+  }
+
+  @Test def testTapirRoutes(): Unit = {
+    val interpreter = PekkoHttpServerInterpreter()(system.dispatcher)
+    def makeRoute(input: EndpointInput[Unit]) = {
+      interpreter.toRoute(
+        endpoint.get
+          .in(input)
+          .errorOut(stringBody)
+          .out(stringBody)
+          .serverLogicPure[Future](_ => Right("ok"))
+      )
+    }
+
+    val routes = concat(
+      concat(makeRoute("test" / "1"), makeRoute("test" / "2")),
+      concat(makeRoute("test" / "3"), makeRoute("test" / "4"))
+    )
+
+    test(routes, "/test/4", "GET /test/4")
+  }
+
+  def test(route: Route, path: String, spanName: String): Unit = {
+    val port = PortUtils.findOpenPort
+    val address: URI = buildAddress(port)
+    val binding =
+      Await.result(Http().bindAndHandle(route, "localhost", port), 10.seconds)
+    try {
+      val request = AggregatedHttpRequest.of(
+        HttpMethod.GET,
+        address.resolve(path).toString
+      )
+      val response = client.execute(request).aggregate.join
+      assertThat(response.status.code).isEqualTo(200)
+      assertThat(response.contentUtf8).isEqualTo("ok")
+
+      testing.waitAndAssertTraces(new Consumer[TraceAssert] {
+        override def accept(trace: TraceAssert): Unit =
+          trace.hasSpansSatisfyingExactly(new Consumer[SpanDataAssert] {
+            override def accept(span: SpanDataAssert): Unit = {
+              span.hasName(spanName)
+            }
+          })
+      })
+    } finally {
+      binding.unbind()
+    }
+  }
+
+  @AfterAll
+  def cleanUp(): Unit = {
+    system.terminate()
+  }
+}
diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerRouteTest.scala b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerRouteTest.scala
index a1bb40627002..cbaaa797042e 100644
--- a/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerRouteTest.scala
+++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerRouteTest.scala
@@ -31,10 +31,8 @@ import org.junit.jupiter.api.{AfterAll, Test, TestInstance}
 
 import java.net.{URI, URISyntaxException}
 import java.util.function.Consumer
-import scala.concurrent.{Await, ExecutionContext, Future}
+import scala.concurrent.Await
 import scala.concurrent.duration.DurationInt
-import sttp.tapir._
-import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter
 
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
 class PekkoHttpServerRouteTest {
@@ -79,26 +77,6 @@ class PekkoHttpServerRouteTest {
     test(route, "/test/1", "GET /test/*")
   }
 
-  @Test def testTapirRoutes(): Unit = {
-    val interpreter = PekkoHttpServerInterpreter()(system.dispatcher)
-    def makeRoute(input: EndpointInput[Unit]) = {
-      interpreter.toRoute(
-        endpoint.get
-          .in(input)
-          .errorOut(stringBody)
-          .out(stringBody)
-          .serverLogicPure[Future](_ => Right("ok"))
-      )
-    }
-
-    val routes = concat(
-      concat(makeRoute("test" / "1"), makeRoute("test" / "2")),
-      concat(makeRoute("test" / "3"), makeRoute("test" / "4"))
-    )
-
-    test(routes, "/test/4", "GET")
-  }
-
   def test(route: Route, path: String, spanName: String): Unit = {
     val port = PortUtils.findOpenPort
     val address: URI = buildAddress(port)


Reviews:
user: login: masonedmison
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-01T00:19:09Z
user: login: masonedmison
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-01T00:21:08Z
user: login: masonedmison
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-01T19:51:51Z
user: login: laurit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T17:25:18Z
user: login: laurit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T17:32:44Z
user: login: masonedmison
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T18:40:48Z
user: login: laurit
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T08:11:36Z

Review comments:
diff_hunk: @@ -10,28 +10,51 @@ muzzle {
     versions.set("[1.0,)")
     assertInverse.set(true)
     extraDependency("org.apache.pekko:pekko-stream_2.12:1.0.1")
+    excludeInstrumentationName("tapir-pekko-http-server")
   }
   pass {
     group.set("org.apache.pekko")
     module.set("pekko-http_2.13")
     versions.set("[1.0,)")
     assertInverse.set(true)
     extraDependency("org.apache.pekko:pekko-stream_2.13:1.0.1")
+    excludeInstrumentationName("tapir-pekko-http-server")
   }
   pass {
     group.set("org.apache.pekko")
     module.set("pekko-http_3")
     versions.set("[1.0,)")
     assertInverse.set(true)
     extraDependency("org.apache.pekko:pekko-stream_3:1.0.1")
+    excludeInstrumentationName("tapir-pekko-http-server")
+  }
+  pass {
+    group.set("com.softwaremill.sttp.tapir")
+    module.set("tapir-pekko-http-server_2.12")
+    versions.set("[1.7,)")
+    assertInverse.set(true)
+    excludeInstrumentationName("pekko-http-server")
+  }
+  pass {
+    group.set("com.softwaremill.sttp.tapir")
+    module.set("tapir-pekko-http-server_2.13")
+    versions.set("[1.7,)")
+    assertInverse.set(true)
+    excludeInstrumentationName("pekko-http-server")
+  }
+  pass {
+    group.set("com.softwaremill.sttp.tapir")
+    module.set("tapir-pekko-http-server_3")
+    versions.set("[1.7,)")
+    assertInverse.set(true)
+    excludeInstrumentationName("pekko-http-server")
   }
 }
 
 dependencies {
   library("org.apache.pekko:pekko-http_2.12:1.0.0")
   library("org.apache.pekko:pekko-stream_2.12:1.0.1")
-
-  testImplementation("com.softwaremill.sttp.tapir:tapir-pekko-http-server_2.12:1.7.0")
+  library("com.softwaremill.sttp.tapir:tapir-pekko-http-server_2.12:1.7.0")
path: instrumentation/pekko/pekko-http-1.0/javaagent/build.gradle.kts
user: login: masonedmison
body: Would it be better to make this `compileOnly` + `testImplementation`?
created_at: 2025-03-01T00:19:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route;
+
+import static java.util.Arrays.asList;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule;
+import java.util.List;
+
+@AutoService(InstrumentationModule.class)
+public class TapirPekkoHttpServerRouteInstrumentationModule extends InstrumentationModule
path: instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/TapirPekkoHttpServerRouteInstrumentationModule.java
user: login: masonedmison
body: Would it make sense to override `classLoadMatcher` here to prevent _both_ this and `PekkoHttpServerRouteIntrumentation` from "firing" in the situation of tapir/pekko?
created_at: 2025-03-01T00:21:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route;
+
+import static java.util.Arrays.asList;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule;
+import java.util.List;
+
+@AutoService(InstrumentationModule.class)
+public class TapirPekkoHttpServerRouteInstrumentationModule extends InstrumentationModule
path: instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/TapirPekkoHttpServerRouteInstrumentationModule.java
user: login: masonedmison
body: @laurit I did some exploring here and there doesn't seem to be "double-instrumentation"--I inspected the general bytebode in the case where Tapir is on the classpath and it looks correct.
created_at: 2025-03-01T19:51:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,28 +10,51 @@ muzzle {
     versions.set("[1.0,)")
     assertInverse.set(true)
     extraDependency("org.apache.pekko:pekko-stream_2.12:1.0.1")
+    excludeInstrumentationName("tapir-pekko-http-server")
   }
   pass {
     group.set("org.apache.pekko")
     module.set("pekko-http_2.13")
     versions.set("[1.0,)")
     assertInverse.set(true)
     extraDependency("org.apache.pekko:pekko-stream_2.13:1.0.1")
+    excludeInstrumentationName("tapir-pekko-http-server")
   }
   pass {
     group.set("org.apache.pekko")
     module.set("pekko-http_3")
     versions.set("[1.0,)")
     assertInverse.set(true)
     extraDependency("org.apache.pekko:pekko-stream_3:1.0.1")
+    excludeInstrumentationName("tapir-pekko-http-server")
+  }
+  pass {
+    group.set("com.softwaremill.sttp.tapir")
+    module.set("tapir-pekko-http-server_2.12")
+    versions.set("[1.7,)")
+    assertInverse.set(true)
+    excludeInstrumentationName("pekko-http-server")
+  }
+  pass {
+    group.set("com.softwaremill.sttp.tapir")
+    module.set("tapir-pekko-http-server_2.13")
+    versions.set("[1.7,)")
+    assertInverse.set(true)
+    excludeInstrumentationName("pekko-http-server")
+  }
+  pass {
+    group.set("com.softwaremill.sttp.tapir")
+    module.set("tapir-pekko-http-server_3")
+    versions.set("[1.7,)")
+    assertInverse.set(true)
+    excludeInstrumentationName("pekko-http-server")
   }
 }
 
 dependencies {
   library("org.apache.pekko:pekko-http_2.12:1.0.0")
   library("org.apache.pekko:pekko-stream_2.12:1.0.1")
-
-  testImplementation("com.softwaremill.sttp.tapir:tapir-pekko-http-server_2.12:1.7.0")
+  library("com.softwaremill.sttp.tapir:tapir-pekko-http-server_2.12:1.7.0")
path: instrumentation/pekko/pekko-http-1.0/javaagent/build.gradle.kts
user: login: laurit
body: `library` is our custom scope that is a shorthand for `compileOnly` + `testImplementation`. I moved tapir tests to a separate suite and changed this to `compileOnly`.
created_at: 2025-03-06T17:25:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route;
+
+import static java.util.Arrays.asList;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule;
+import java.util.List;
+
+@AutoService(InstrumentationModule.class)
+public class TapirPekkoHttpServerRouteInstrumentationModule extends InstrumentationModule
path: instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/TapirPekkoHttpServerRouteInstrumentationModule.java
user: login: laurit
body: as far as I can tell `RouteConcatenationInstrumentation` is applied twice, removed it from the tapir module
created_at: 2025-03-06T17:32:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route;
+
+import static java.util.Arrays.asList;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule;
+import java.util.List;
+
+@AutoService(InstrumentationModule.class)
+public class TapirPekkoHttpServerRouteInstrumentationModule extends InstrumentationModule
path: instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/TapirPekkoHttpServerRouteInstrumentationModule.java
user: login: masonedmison
body: Apologies for that. I'm curious: how did you determine that?
created_at: 2025-03-06T18:40:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: masonedmison
created_at: 2025-02-27T20:20:10Z
body: @laurit Would you be able to look at this for me 🙂?
author_association: CONTRIBUTOR
user: login: laurit
created_at: 2025-02-28T17:47:01Z
body: @masonedmison I think with your current approach pekko-http instrumentation will fail to apply when tapir is not available during runtime. You can try this our by changing the tapir dependency to be compileOnly + testCompileOnly. This is because  we have tooling that detects which classes are used by the instrumentation and when those classes are missing instrumentation is not applied. It is probably easiest to treat it as completely separate instrumentation. An alternative would be to have it in the same module as pekko but have it in a different `InstrumentationModule` but then to get muzzle working you may need to use `excludeInstrumentationName` so that you could have separate `pass` blocks for pekko and tapir.
author_association: CONTRIBUTOR
user: login: masonedmison
created_at: 2025-02-28T22:00:02Z
body: > @masonedmison I think with your current approach pekko-http instrumentation will fail to apply when tapir is not available during runtime. You can try this our by changing the tapir dependency to be compileOnly + testCompileOnly. This is because we have tooling that detects which classes are used by the instrumentation and when those classes are missing instrumentation is not applied. It is probably easiest to treat it as completely separate instrumentation. An alternative would be to have it in the same module as pekko but have it in a different `InstrumentationModule` but then to get muzzle working you may need to use `excludeInstrumentationName` so that you could have separate `pass` blocks for pekko and tapir.

@laurit Very good catch--sorry I missed this! I'm pursuing the second approach first (just pushed up a commit): I've created a separate `InstrumentationModule`, and I've added the `excludeInstrumentationName` statements to the muzzle check blocks--with these in place running the muzzle check works. However, I've noticed that when running a simple pekko HTTP simple program (_without_ tapir on the class path) I see a slew of "Missing Class..." on the logs, e.g.,

```
[error] [otel.javaagent 2025-02-28 15:50:51:020 -0600] [main] WARN io.opentelemetry.javaagent.tooling.instrumentation.MuzzleMatcher - Instrumentation skipped, mismatched references were found: tapir-pekko-http [class io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route.TapirPekkoHttpServerRouteInstrumentationModule] on jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7
[error] [otel.javaagent 2025-02-28 15:50:51:021 -0600] [main] WARN io.opentelemetry.javaagent.tooling.instrumentation.MuzzleMatcher - -- io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route.RouteWrapper$Finalizer:47 Missing class sttp.tapir.EndpointInput$Query
[error] [otel.javaagent 2025-02-28 15:50:51:021 -0600] [main] WARN io.opentelemetry.javaagent.tooling.instrumentation.MuzzleMatcher - -- io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route.TapirPathInstrumentation$ApplyAdvice:42 Missing class sttp.tapir.server.ServerEndpoint
[error] [otel.javaagent 2025-02-28 15:50:51:021 -0600] [main] WARN io.opentelemetry.javaagent.tooling.instrumentation.MuzzleMatcher - -- io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route.RouteWrapper$Finalizer:44 Missing class sttp.tapir.EndpointInput$PathCapture
```
Looking at the doc string for `MuzzleMatcher` this looks to be expected, but I just want to make sure.
author_association: CONTRIBUTOR
user: login: masonedmison
created_at: 2025-03-05T14:33:37Z
body: @laurit sorry to be a bother, but would you have a chance to look at my changes following your last comment?
author_association: CONTRIBUTOR
user: login: masonedmison
created_at: 2025-03-07T15:27:57Z
body: @laurit Thank you very much for helping me get this together. I really appreciate it. I see that this is slated to go out in the `2.14.0` release (which is great!); do you know when this release will happen?
author_association: CONTRIBUTOR
user: login: laurit
created_at: 2025-03-07T18:18:27Z
body: > do you know when this release will happen?

second half of next week, see https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/RELEASING.md#release-cadence

author_association: CONTRIBUTOR