Quellcode durchsuchen

Add javaagent<->application context bridge for HttpRouteHolder (#5838)

* Add javaagent<->application context bridge for HttpRouteHolder

* remove comments

* fix broken http.route bridge

* spotless

* Move to a separate module
Mateusz Rzeszutek vor 2 Jahren
Ursprung
Commit
2bb7873f99
21 geänderte Dateien mit 942 neuen und 474 gelöschten Zeilen
  1. 13 20
      instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteHolder.java
  2. 61 0
      instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/internal/HttpRouteState.java
  3. 0 35
      instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/build.gradle.kts
  4. 0 212
      instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/AgentContextStorage.java
  5. 104 0
      instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/AgentContextWrapper.java
  6. 118 0
      instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/ContextKeyBridge.java
  7. 202 0
      instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/InstrumentationApiContextBridging.java
  8. 0 44
      instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextBridgeTest.groovy
  9. 0 119
      instrumentation/opentelemetry-api/opentelemetry-api-1.0/testing/src/main/java/AgentSpanTestingInstrumenter.java
  10. 45 0
      instrumentation/opentelemetry-instrumentation-api/javaagent/build.gradle.kts
  11. 57 0
      instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/HttpRouteStateInstrumentation.java
  12. 34 0
      instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/InstrumentationApiInstrumentationModule.java
  13. 115 0
      instrumentation/opentelemetry-instrumentation-api/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/ContextBridgeTest.java
  14. 4 1
      instrumentation/opentelemetry-instrumentation-api/javaagent/src/testOldServerSpan/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/LegacyServerSpanContextBridgeTest.java
  15. 0 0
      instrumentation/opentelemetry-instrumentation-api/testing/build.gradle.kts
  16. 3 9
      instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTesting.java
  17. 5 27
      instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTestingInstrumentation.java
  18. 3 6
      instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTestingInstrumentationModule.java
  19. 82 0
      instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTestingInstrumenter.java
  20. 94 0
      instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/MockHttpServerAttributesGetter.java
  21. 2 1
      settings.gradle.kts

+ 13 - 20
instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteHolder.java

@@ -7,10 +7,10 @@ package io.opentelemetry.instrumentation.api.instrumenter.http;
 
 import io.opentelemetry.api.trace.Span;
 import io.opentelemetry.context.Context;
-import io.opentelemetry.context.ContextKey;
 import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer;
 import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
 import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan;
+import io.opentelemetry.instrumentation.api.internal.HttpRouteState;
 import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
 import javax.annotation.Nullable;
 
@@ -25,25 +25,19 @@ import javax.annotation.Nullable;
  */
 public final class HttpRouteHolder {
 
-  private static final ContextKey<HttpRouteHolder> CONTEXT_KEY =
-      ContextKey.named("opentelemetry-http-server-route-key");
-
   /**
    * Returns a {@link ContextCustomizer} that initializes a {@link HttpRouteHolder} in the {@link
    * Context} returned from {@link Instrumenter#start(Context, Object)}.
    */
   public static <REQUEST> ContextCustomizer<REQUEST> get() {
     return (context, request, startAttributes) -> {
-      if (context.get(CONTEXT_KEY) != null) {
+      if (HttpRouteState.fromContextOrNull(context) != null) {
         return context;
       }
-      return context.with(CONTEXT_KEY, new HttpRouteHolder());
+      return context.with(HttpRouteState.create(0, null));
     };
   }
 
-  private volatile int updatedBySourceOrder = 0;
-  @Nullable private volatile String route;
-
   private HttpRouteHolder() {}
 
   /**
@@ -107,8 +101,8 @@ public final class HttpRouteHolder {
     if (serverSpan == null) {
       return;
     }
-    HttpRouteHolder httpRouteHolder = context.get(CONTEXT_KEY);
-    if (httpRouteHolder == null) {
+    HttpRouteState httpRouteState = HttpRouteState.fromContextOrNull(context);
+    if (httpRouteState == null) {
       String httpRoute = httpRouteGetter.get(context, arg1, arg2);
       if (httpRoute != null && !httpRoute.isEmpty()) {
         // update both span name and attribute, since there's no HttpRouteHolder in the context
@@ -120,27 +114,26 @@ public final class HttpRouteHolder {
     // special case for servlet filters, even when we have a route from previous filter see whether
     // the new route is better and if so use it instead
     boolean onlyIfBetterRoute =
-        !source.useFirst && source.order == httpRouteHolder.updatedBySourceOrder;
-    if (source.order > httpRouteHolder.updatedBySourceOrder || onlyIfBetterRoute) {
+        !source.useFirst && source.order == httpRouteState.getUpdatedBySourceOrder();
+    if (source.order > httpRouteState.getUpdatedBySourceOrder() || onlyIfBetterRoute) {
       String route = httpRouteGetter.get(context, arg1, arg2);
       if (route != null
           && !route.isEmpty()
-          && (!onlyIfBetterRoute || httpRouteHolder.isBetterRoute(route))) {
+          && (!onlyIfBetterRoute || isBetterRoute(httpRouteState, route))) {
 
         // update just the span name - the attribute will be picked up by the
         // HttpServerAttributesExtractor at the end of request processing
         serverSpan.updateName(route);
 
-        httpRouteHolder.updatedBySourceOrder = source.order;
-        httpRouteHolder.route = route;
+        httpRouteState.update(context, source.order, route);
       }
     }
   }
 
   // This is used when setting route from a servlet filter to pick the most descriptive (longest)
   // route.
-  private boolean isBetterRoute(String name) {
-    String route = this.route;
+  private static boolean isBetterRoute(HttpRouteState httpRouteState, String name) {
+    String route = httpRouteState.getRoute();
     int routeLength = route == null ? 0 : route.length();
     return name.length() > routeLength;
   }
@@ -151,8 +144,8 @@ public final class HttpRouteHolder {
    */
   @Nullable
   static String getRoute(Context context) {
-    HttpRouteHolder httpRouteHolder = context.get(CONTEXT_KEY);
-    return httpRouteHolder == null ? null : httpRouteHolder.route;
+    HttpRouteState httpRouteState = HttpRouteState.fromContextOrNull(context);
+    return httpRouteState == null ? null : httpRouteState.getRoute();
   }
 
   private static final class OneArgAdapter<T> implements HttpRouteBiGetter<T, HttpRouteGetter<T>> {

+ 61 - 0
instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/internal/HttpRouteState.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.internal;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.ContextKey;
+import io.opentelemetry.context.ImplicitContextKeyed;
+import javax.annotation.Nullable;
+
+/**
+ * This class is internal and is hence not for public use. Its APIs are unstable and can change at
+ * any time.
+ */
+public final class HttpRouteState implements ImplicitContextKeyed {
+
+  private static final ContextKey<HttpRouteState> KEY =
+      ContextKey.named("opentelemetry-http-server-route-key");
+
+  @Nullable
+  public static HttpRouteState fromContextOrNull(Context context) {
+    return context.get(KEY);
+  }
+
+  public static HttpRouteState create(int updatedBySourceOrder, @Nullable String route) {
+    return new HttpRouteState(updatedBySourceOrder, route);
+  }
+
+  private volatile int updatedBySourceOrder;
+  @Nullable private volatile String route;
+
+  private HttpRouteState(int updatedBySourceOrder, @Nullable String route) {
+    this.updatedBySourceOrder = updatedBySourceOrder;
+    this.route = route;
+  }
+
+  @Override
+  public Context storeInContext(Context context) {
+    return context.with(KEY, this);
+  }
+
+  public int getUpdatedBySourceOrder() {
+    return updatedBySourceOrder;
+  }
+
+  @Nullable
+  public String getRoute() {
+    return route;
+  }
+
+  public void update(
+      @SuppressWarnings("unused")
+          Context context, // context is used by the javaagent bridge instrumentation
+      int updatedBySourceOrder,
+      String route) {
+    this.updatedBySourceOrder = updatedBySourceOrder;
+    this.route = route;
+  }
+}

+ 0 - 35
instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/build.gradle.kts

@@ -42,39 +42,4 @@ dependencies {
   // @WithSpan annotation is used to generate spans in ContextBridgeTest
   testImplementation("io.opentelemetry:opentelemetry-extension-annotations")
   testInstrumentation(project(":instrumentation:opentelemetry-annotations-1.0:javaagent"))
-
-  testImplementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:testing"))
-  testInstrumentation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:testing"))
-}
-
-testing {
-  suites {
-    val testOldInstrumentationApi by registering(JvmTestSuite::class) {
-      dependencies {
-        implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv")
-        implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api")
-        implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:testing"))
-      }
-    }
-  }
-}
-
-configurations.configureEach {
-  if (name.contains("testOldInstrumentationApi")) {
-    resolutionStrategy {
-      dependencySubstitution {
-        // version 1.13.0 contains the old ServerSpan implementation that uses SERVER_KEY context key
-        substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv"))
-          .using(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv:1.13.0-alpha"))
-        substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api"))
-          .using(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:1.13.0-alpha"))
-      }
-    }
-  }
-}
-
-tasks {
-  check {
-    dependsOn(testing.suites)
-  }
 }

+ 0 - 212
instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/AgentContextStorage.java

@@ -10,18 +10,13 @@ import static java.util.logging.Level.FINE;
 import application.io.opentelemetry.api.baggage.Baggage;
 import application.io.opentelemetry.api.trace.Span;
 import application.io.opentelemetry.context.Context;
-import application.io.opentelemetry.context.ContextKey;
 import application.io.opentelemetry.context.ContextStorage;
 import application.io.opentelemetry.context.Scope;
-import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.baggage.BaggageBridging;
-import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging;
 import java.lang.invoke.MethodHandle;
 import java.lang.invoke.MethodHandles;
 import java.lang.invoke.MethodType;
-import java.lang.reflect.Field;
 import java.util.function.Function;
 import java.util.logging.Logger;
-import javax.annotation.Nullable;
 
 /**
  * {@link ContextStorage} which stores the {@link Context} in the user's application inside the
@@ -144,59 +139,6 @@ public class AgentContextStorage implements ContextStorage, AutoCloseable {
   static final io.opentelemetry.context.ContextKey<Context> APPLICATION_CONTEXT =
       io.opentelemetry.context.ContextKey.named("otel-context");
 
-  @SuppressWarnings({"unchecked", "rawtypes"})
-  static final ContextKeyBridge<?, ?>[] CONTEXT_KEY_BRIDGES =
-      new ContextKeyBridge[] {
-        new ContextKeyBridge<Span, io.opentelemetry.api.trace.Span>(
-            "application.io.opentelemetry.api.trace.SpanContextKey",
-            "io.opentelemetry.api.trace.SpanContextKey",
-            Bridging::toApplication,
-            Bridging::toAgentOrNull),
-        new ContextKeyBridge<>(
-            "application.io.opentelemetry.api.baggage.BaggageContextKey",
-            "io.opentelemetry.api.baggage.BaggageContextKey",
-            BaggageBridging::toApplication,
-            BaggageBridging::toAgent),
-        new ContextKeyBridge<Span, io.opentelemetry.api.trace.Span>(
-            "application.io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan",
-            "io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan",
-            Bridging::toApplication,
-            Bridging::toAgentOrNull),
-        // old SERVER_KEY bridge - needed to make legacy ServerSpan work, for users who're using old
-        // instrumentation-api version with the newest agent version
-        new ContextKeyBridge<Span, io.opentelemetry.api.trace.Span>(
-            "application.io.opentelemetry.instrumentation.api.internal.SpanKey",
-            "io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan",
-            "SERVER_KEY",
-            "KEY",
-            Bridging::toApplication,
-            Bridging::toAgentOrNull),
-        // span kind keys
-        bridgeSpanKey("KIND_SERVER_KEY"),
-        bridgeSpanKey("KIND_CLIENT_KEY"),
-        bridgeSpanKey("KIND_CONSUMER_KEY"),
-        bridgeSpanKey("KIND_PRODUCER_KEY"),
-        // semantic convention keys
-        bridgeSpanKey("HTTP_SERVER_KEY"),
-        bridgeSpanKey("RPC_SERVER_KEY"),
-        bridgeSpanKey("HTTP_CLIENT_KEY"),
-        bridgeSpanKey("RPC_CLIENT_KEY"),
-        bridgeSpanKey("DB_CLIENT_KEY"),
-        bridgeSpanKey("PRODUCER_KEY"),
-        bridgeSpanKey("CONSUMER_RECEIVE_KEY"),
-        bridgeSpanKey("CONSUMER_PROCESS_KEY"),
-      };
-
-  private static ContextKeyBridge<Span, io.opentelemetry.api.trace.Span> bridgeSpanKey(
-      String name) {
-    return new ContextKeyBridge<>(
-        "application.io.opentelemetry.instrumentation.api.internal.SpanKey",
-        "io.opentelemetry.instrumentation.api.internal.SpanKey",
-        name,
-        Bridging::toApplication,
-        Bridging::toAgentOrNull);
-  }
-
   @Override
   public Scope attach(Context toAttach) {
     io.opentelemetry.context.Context currentAgentContext =
@@ -248,158 +190,4 @@ public class AgentContextStorage implements ContextStorage, AutoCloseable {
       ((AutoCloseable) agentStorage).close();
     }
   }
-
-  private static class AgentContextWrapper implements Context {
-    final io.opentelemetry.context.Context agentContext;
-    final Context applicationContext;
-
-    AgentContextWrapper(io.opentelemetry.context.Context agentContext) {
-      this(agentContext, agentContext.get(APPLICATION_CONTEXT));
-    }
-
-    AgentContextWrapper(io.opentelemetry.context.Context agentContext, Context applicationContext) {
-      if (applicationContext instanceof AgentContextWrapper) {
-        throw new IllegalStateException("Expected unwrapped context");
-      }
-      this.agentContext = agentContext;
-      this.applicationContext = applicationContext;
-    }
-
-    io.opentelemetry.context.Context toAgentContext() {
-      if (agentContext.get(APPLICATION_CONTEXT) == applicationContext) {
-        return agentContext;
-      }
-      return agentContext.with(APPLICATION_CONTEXT, applicationContext);
-    }
-
-    @Override
-    public <V> V get(ContextKey<V> key) {
-      for (ContextKeyBridge<?, ?> bridge : CONTEXT_KEY_BRIDGES) {
-        V value = bridge.get(this, key);
-        if (value != null) {
-          return value;
-        }
-      }
-
-      return applicationContext.get(key);
-    }
-
-    @Override
-    public <V> Context with(ContextKey<V> k1, V v1) {
-      for (ContextKeyBridge<?, ?> bridge : CONTEXT_KEY_BRIDGES) {
-        Context context = bridge.with(this, k1, v1);
-        if (context != null) {
-          return context;
-        }
-      }
-      return new AgentContextWrapper(agentContext, applicationContext.with(k1, v1));
-    }
-
-    @Override
-    public String toString() {
-      return "AgentContextWrapper{agentContext="
-          + agentContext
-          + ", applicationContext="
-          + applicationContext
-          + "}";
-    }
-  }
-
-  static class ContextKeyBridge<APPLICATION, AGENT> {
-
-    private final ContextKey<APPLICATION> applicationContextKey;
-    private final io.opentelemetry.context.ContextKey<AGENT> agentContextKey;
-    private final Function<APPLICATION, AGENT> toAgent;
-    private final Function<AGENT, APPLICATION> toApplication;
-
-    ContextKeyBridge(
-        String applicationKeyHolderClassName,
-        String agentKeyHolderClassName,
-        Function<AGENT, APPLICATION> toApplication,
-        Function<APPLICATION, AGENT> toAgent) {
-      this(applicationKeyHolderClassName, agentKeyHolderClassName, "KEY", toApplication, toAgent);
-    }
-
-    ContextKeyBridge(
-        String applicationKeyHolderClassName,
-        String agentKeyHolderClassName,
-        String fieldName,
-        Function<AGENT, APPLICATION> toApplication,
-        Function<APPLICATION, AGENT> toAgent) {
-      this(
-          applicationKeyHolderClassName,
-          agentKeyHolderClassName,
-          fieldName,
-          fieldName,
-          toApplication,
-          toAgent);
-    }
-
-    @SuppressWarnings("unchecked")
-    ContextKeyBridge(
-        String applicationKeyHolderClassName,
-        String agentKeyHolderClassName,
-        String applicationFieldName,
-        String agentFieldName,
-        Function<AGENT, APPLICATION> toApplication,
-        Function<APPLICATION, AGENT> toAgent) {
-      this.toApplication = toApplication;
-      this.toAgent = toAgent;
-
-      ContextKey<APPLICATION> applicationContextKey;
-      try {
-        Class<?> applicationKeyHolderClass = Class.forName(applicationKeyHolderClassName);
-        Field applicationContextKeyField =
-            applicationKeyHolderClass.getDeclaredField(applicationFieldName);
-        applicationContextKeyField.setAccessible(true);
-        applicationContextKey = (ContextKey<APPLICATION>) applicationContextKeyField.get(null);
-      } catch (Throwable t) {
-        applicationContextKey = null;
-      }
-      this.applicationContextKey = applicationContextKey;
-
-      io.opentelemetry.context.ContextKey<AGENT> agentContextKey;
-      try {
-        Class<?> agentKeyHolderClass = Class.forName(agentKeyHolderClassName);
-        Field agentContextKeyField = agentKeyHolderClass.getDeclaredField(agentFieldName);
-        agentContextKeyField.setAccessible(true);
-        agentContextKey =
-            (io.opentelemetry.context.ContextKey<AGENT>) agentContextKeyField.get(null);
-      } catch (Throwable t) {
-        agentContextKey = null;
-      }
-      this.agentContextKey = agentContextKey;
-    }
-
-    @Nullable
-    <V> V get(AgentContextWrapper contextWrapper, ContextKey<V> requestedKey) {
-      if (requestedKey == applicationContextKey) {
-        AGENT agentValue = contextWrapper.agentContext.get(agentContextKey);
-        if (agentValue == null) {
-          return null;
-        }
-        APPLICATION applicationValue = toApplication.apply(agentValue);
-        @SuppressWarnings("unchecked")
-        V castValue = (V) applicationValue;
-        return castValue;
-      }
-      return null;
-    }
-
-    @Nullable
-    <V> Context with(AgentContextWrapper contextWrapper, ContextKey<V> requestedKey, V value) {
-      if (requestedKey == applicationContextKey) {
-        @SuppressWarnings("unchecked")
-        APPLICATION applicationValue = (APPLICATION) value;
-        AGENT agentValue = toAgent.apply(applicationValue);
-        if (agentValue == null) {
-          return contextWrapper;
-        }
-        return new AgentContextWrapper(
-            contextWrapper.agentContext.with(agentContextKey, agentValue),
-            contextWrapper.applicationContext);
-      }
-      return null;
-    }
-  }
 }

+ 104 - 0
instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/AgentContextWrapper.java

@@ -0,0 +1,104 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context;
+
+import application.io.opentelemetry.api.trace.Span;
+import application.io.opentelemetry.context.Context;
+import application.io.opentelemetry.context.ContextKey;
+import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.baggage.BaggageBridging;
+import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+final class AgentContextWrapper implements Context {
+
+  static final List<ContextKeyBridge<?, ?>> CONTEXT_KEY_BRIDGES;
+
+  static {
+    List<ContextKeyBridge<?, ?>> bridges = new ArrayList<>();
+    try {
+      bridges.add(
+          new ContextKeyBridge<Span, io.opentelemetry.api.trace.Span>(
+              "application.io.opentelemetry.api.trace.SpanContextKey",
+              "io.opentelemetry.api.trace.SpanContextKey",
+              Bridging::toApplication,
+              Bridging::toAgentOrNull));
+    } catch (Throwable ignored) {
+      // reflection error; in practice should never happen, we can ignore it
+    }
+    try {
+      bridges.add(
+          new ContextKeyBridge<>(
+              "application.io.opentelemetry.api.baggage.BaggageContextKey",
+              "io.opentelemetry.api.baggage.BaggageContextKey",
+              BaggageBridging::toApplication,
+              BaggageBridging::toAgent));
+    } catch (Throwable ignored) {
+      // reflection error; in practice should never happen, we can ignore it
+    }
+    bridges.addAll(InstrumentationApiContextBridging.instrumentationApiBridges());
+    CONTEXT_KEY_BRIDGES = Collections.unmodifiableList(bridges);
+  }
+
+  final io.opentelemetry.context.Context agentContext;
+  final Context applicationContext;
+
+  AgentContextWrapper(io.opentelemetry.context.Context agentContext) {
+    this(agentContext, agentContext.get(AgentContextStorage.APPLICATION_CONTEXT));
+  }
+
+  AgentContextWrapper(io.opentelemetry.context.Context agentContext, Context applicationContext) {
+    if (applicationContext instanceof AgentContextWrapper) {
+      throw new IllegalStateException("Expected unwrapped context");
+    }
+    this.agentContext = agentContext;
+    this.applicationContext = applicationContext;
+  }
+
+  io.opentelemetry.context.Context toAgentContext() {
+    if (agentContext.get(AgentContextStorage.APPLICATION_CONTEXT) == applicationContext) {
+      return agentContext;
+    }
+    return agentContext.with(AgentContextStorage.APPLICATION_CONTEXT, applicationContext);
+  }
+
+  public io.opentelemetry.context.Context getAgentContext() {
+    return agentContext;
+  }
+
+  @Override
+  public <V> V get(ContextKey<V> key) {
+    for (ContextKeyBridge<?, ?> bridge : CONTEXT_KEY_BRIDGES) {
+      V value = bridge.get(this, key);
+      if (value != null) {
+        return value;
+      }
+    }
+
+    return applicationContext.get(key);
+  }
+
+  @Override
+  public <V> Context with(ContextKey<V> k1, V v1) {
+    for (ContextKeyBridge<?, ?> bridge : CONTEXT_KEY_BRIDGES) {
+      Context context = bridge.with(this, k1, v1);
+      if (context != null) {
+        return context;
+      }
+    }
+    return new AgentContextWrapper(agentContext, applicationContext.with(k1, v1));
+  }
+
+  @Override
+  public String toString() {
+    return "AgentContextWrapper{agentContext="
+        + agentContext
+        + ", applicationContext="
+        + applicationContext
+        + "}";
+  }
+}

+ 118 - 0
instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/ContextKeyBridge.java

@@ -0,0 +1,118 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context;
+
+import application.io.opentelemetry.context.Context;
+import application.io.opentelemetry.context.ContextKey;
+import java.lang.reflect.Field;
+import java.util.function.Function;
+import javax.annotation.Nullable;
+
+final class ContextKeyBridge<APPLICATION, AGENT> {
+
+  private final ContextKey<APPLICATION> applicationContextKey;
+  private final io.opentelemetry.context.ContextKey<AGENT> agentContextKey;
+  private final Function<APPLICATION, AGENT> toAgent;
+  private final Function<AGENT, APPLICATION> toApplication;
+
+  // TODO: maybe add a builder instead of all those constructors?
+  ContextKeyBridge(
+      String applicationKeyHolderClassName,
+      String agentKeyHolderClassName,
+      Function<AGENT, APPLICATION> toApplication,
+      Function<APPLICATION, AGENT> toAgent)
+      throws Throwable {
+    this(applicationKeyHolderClassName, agentKeyHolderClassName, "KEY", toApplication, toAgent);
+  }
+
+  ContextKeyBridge(
+      String applicationKeyHolderClassName,
+      String agentKeyHolderClassName,
+      String fieldName,
+      Function<AGENT, APPLICATION> toApplication,
+      Function<APPLICATION, AGENT> toAgent)
+      throws Throwable {
+    this(
+        applicationKeyHolderClassName,
+        agentKeyHolderClassName,
+        fieldName,
+        fieldName,
+        toApplication,
+        toAgent);
+  }
+
+  ContextKeyBridge(
+      String applicationKeyHolderClassName,
+      String agentKeyHolderClassName,
+      String applicationFieldName,
+      String agentFieldName,
+      Function<AGENT, APPLICATION> toApplication,
+      Function<APPLICATION, AGENT> toAgent)
+      throws Throwable {
+    this(
+        Class.forName(applicationKeyHolderClassName),
+        Class.forName(agentKeyHolderClassName),
+        applicationFieldName,
+        agentFieldName,
+        toApplication,
+        toAgent);
+  }
+
+  @SuppressWarnings("unchecked")
+  ContextKeyBridge(
+      Class<?> applicationKeyHolderClass,
+      Class<?> agentKeyHolderClass,
+      String applicationFieldName,
+      String agentFieldName,
+      Function<AGENT, APPLICATION> toApplication,
+      Function<APPLICATION, AGENT> toAgent)
+      throws Throwable {
+
+    Field applicationContextKeyField =
+        applicationKeyHolderClass.getDeclaredField(applicationFieldName);
+    applicationContextKeyField.setAccessible(true);
+    this.applicationContextKey = (ContextKey<APPLICATION>) applicationContextKeyField.get(null);
+
+    Field agentContextKeyField = agentKeyHolderClass.getDeclaredField(agentFieldName);
+    agentContextKeyField.setAccessible(true);
+    this.agentContextKey =
+        (io.opentelemetry.context.ContextKey<AGENT>) agentContextKeyField.get(null);
+
+    this.toApplication = toApplication;
+    this.toAgent = toAgent;
+  }
+
+  @Nullable
+  <V> V get(AgentContextWrapper contextWrapper, ContextKey<V> requestedKey) {
+    if (requestedKey == applicationContextKey) {
+      AGENT agentValue = contextWrapper.agentContext.get(agentContextKey);
+      if (agentValue == null) {
+        return null;
+      }
+      APPLICATION applicationValue = toApplication.apply(agentValue);
+      @SuppressWarnings("unchecked")
+      V castValue = (V) applicationValue;
+      return castValue;
+    }
+    return null;
+  }
+
+  @Nullable
+  <V> Context with(AgentContextWrapper contextWrapper, ContextKey<V> requestedKey, V value) {
+    if (requestedKey == applicationContextKey) {
+      @SuppressWarnings("unchecked")
+      APPLICATION applicationValue = (APPLICATION) value;
+      AGENT agentValue = toAgent.apply(applicationValue);
+      if (agentValue == null) {
+        return contextWrapper;
+      }
+      return new AgentContextWrapper(
+          contextWrapper.agentContext.with(agentContextKey, agentValue),
+          contextWrapper.applicationContext);
+    }
+    return null;
+  }
+}

+ 202 - 0
instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/InstrumentationApiContextBridging.java

@@ -0,0 +1,202 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context;
+
+import application.io.opentelemetry.api.trace.Span;
+import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+import javax.annotation.Nullable;
+
+@SuppressWarnings({"unchecked", "rawtypes"})
+final class InstrumentationApiContextBridging {
+
+  static List<ContextKeyBridge<?, ?>> instrumentationApiBridges() {
+    List<ContextKeyBridge<?, ?>> bridges = new ArrayList<>();
+
+    try {
+      bridges.add(
+          new ContextKeyBridge<Span, io.opentelemetry.api.trace.Span>(
+              "application.io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan",
+              "io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan",
+              Bridging::toApplication,
+              Bridging::toAgentOrNull));
+    } catch (Throwable e) {
+      // no instrumentation-api on classpath
+    }
+
+    try {
+      // old SERVER_KEY bridge - needed to make legacy ServerSpan work, for users who're using old
+      // instrumentation-api version with the newest agent version
+      bridges.add(
+          new ContextKeyBridge<Span, io.opentelemetry.api.trace.Span>(
+              "application.io.opentelemetry.instrumentation.api.internal.SpanKey",
+              "io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan",
+              "SERVER_KEY",
+              "KEY",
+              Bridging::toApplication,
+              Bridging::toAgentOrNull));
+    } catch (Throwable e) {
+      // no old instrumentation-api on classpath
+    }
+
+    List<String> spanKeyNames =
+        Arrays.asList(
+            // span kind keys
+            "KIND_SERVER_KEY",
+            "KIND_CLIENT_KEY",
+            "KIND_CONSUMER_KEY",
+            "KIND_PRODUCER_KEY",
+            // semantic convention keys
+            "HTTP_SERVER_KEY",
+            "RPC_SERVER_KEY",
+            "HTTP_CLIENT_KEY",
+            "RPC_CLIENT_KEY",
+            "DB_CLIENT_KEY",
+            "PRODUCER_KEY",
+            "CONSUMER_RECEIVE_KEY",
+            "CONSUMER_PROCESS_KEY");
+
+    for (String spanKeyName : spanKeyNames) {
+      ContextKeyBridge<?, ?> spanKeyBridge = spanKeyBridge(spanKeyName);
+      if (spanKeyBridge != null) {
+        bridges.add(spanKeyBridge);
+      }
+    }
+
+    ContextKeyBridge<?, ?> httpRouteHolderBridge = httpRouteStateBridge();
+    if (httpRouteHolderBridge != null) {
+      bridges.add(httpRouteHolderBridge);
+    }
+
+    return bridges;
+  }
+
+  @Nullable
+  private static ContextKeyBridge<Span, io.opentelemetry.api.trace.Span> spanKeyBridge(
+      String name) {
+    try {
+      return new ContextKeyBridge<>(
+          "application.io.opentelemetry.instrumentation.api.internal.SpanKey",
+          "io.opentelemetry.instrumentation.api.internal.SpanKey",
+          name,
+          Bridging::toApplication,
+          Bridging::toAgentOrNull);
+    } catch (Throwable e) {
+      // instrumentation-api may be absent on the classpath, just skip
+      return null;
+    }
+  }
+
+  private static final Class<?> AGENT_HTTP_ROUTE_STATE;
+  private static final MethodHandle AGENT_CREATE;
+  private static final MethodHandle AGENT_GET_UPDATED_BY_SOURCE_ORDER;
+  private static final MethodHandle AGENT_GET_ROUTE;
+
+  private static final Class<?> APPLICATION_HTTP_ROUTE_STATE;
+  private static final MethodHandle APPLICATION_CREATE;
+  private static final MethodHandle APPLICATION_GET_UPDATED_BY_SOURCE_ORDER;
+  private static final MethodHandle APPLICATION_GET_ROUTE;
+
+  static {
+    MethodHandles.Lookup lookup = MethodHandles.lookup();
+
+    Class<?> agentHttpRouteState = null;
+    MethodHandle agentCreate = null;
+    MethodHandle agentGetUpdatedBySourceOrder = null;
+    MethodHandle agentGetRoute = null;
+    Class<?> applicationHttpRouteState = null;
+    MethodHandle applicationCreate = null;
+    MethodHandle applicationGetUpdatedBySourceOrder = null;
+    MethodHandle applicationGetRoute = null;
+
+    try {
+      agentHttpRouteState =
+          Class.forName("io.opentelemetry.instrumentation.api.internal.HttpRouteState");
+      agentCreate =
+          lookup.findStatic(
+              agentHttpRouteState,
+              "create",
+              MethodType.methodType(agentHttpRouteState, int.class, String.class));
+      agentGetUpdatedBySourceOrder =
+          lookup.findVirtual(
+              agentHttpRouteState, "getUpdatedBySourceOrder", MethodType.methodType(int.class));
+      agentGetRoute =
+          lookup.findVirtual(agentHttpRouteState, "getRoute", MethodType.methodType(String.class));
+
+      applicationHttpRouteState =
+          Class.forName("application.io.opentelemetry.instrumentation.api.internal.HttpRouteState");
+      applicationCreate =
+          lookup.findStatic(
+              applicationHttpRouteState,
+              "create",
+              MethodType.methodType(applicationHttpRouteState, int.class, String.class));
+      applicationGetUpdatedBySourceOrder =
+          lookup.findVirtual(
+              applicationHttpRouteState,
+              "getUpdatedBySourceOrder",
+              MethodType.methodType(int.class));
+      applicationGetRoute =
+          lookup.findVirtual(
+              applicationHttpRouteState, "getRoute", MethodType.methodType(String.class));
+    } catch (Throwable ignored) {
+      // instrumentation-api may be absent on the classpath, or it might be an older version
+    }
+
+    AGENT_HTTP_ROUTE_STATE = agentHttpRouteState;
+    AGENT_CREATE = agentCreate;
+    AGENT_GET_UPDATED_BY_SOURCE_ORDER = agentGetUpdatedBySourceOrder;
+    AGENT_GET_ROUTE = agentGetRoute;
+    APPLICATION_HTTP_ROUTE_STATE = applicationHttpRouteState;
+    APPLICATION_CREATE = applicationCreate;
+    APPLICATION_GET_UPDATED_BY_SOURCE_ORDER = applicationGetUpdatedBySourceOrder;
+    APPLICATION_GET_ROUTE = applicationGetRoute;
+  }
+
+  @Nullable
+  private static ContextKeyBridge<?, ?> httpRouteStateBridge() {
+    if (APPLICATION_HTTP_ROUTE_STATE == null
+        || APPLICATION_CREATE == null
+        || APPLICATION_GET_UPDATED_BY_SOURCE_ORDER == null
+        || APPLICATION_GET_ROUTE == null) {
+      // HttpRouteHolder not on application classpath; or an old version of it
+      return null;
+    }
+    try {
+      return new ContextKeyBridge<>(
+          APPLICATION_HTTP_ROUTE_STATE,
+          AGENT_HTTP_ROUTE_STATE,
+          "KEY",
+          "KEY",
+          httpRouteStateConvert(
+              APPLICATION_CREATE, AGENT_GET_UPDATED_BY_SOURCE_ORDER, AGENT_GET_ROUTE),
+          httpRouteStateConvert(
+              AGENT_CREATE, APPLICATION_GET_UPDATED_BY_SOURCE_ORDER, APPLICATION_GET_ROUTE));
+    } catch (Throwable ignored) {
+      return null;
+    }
+  }
+
+  private static Function<Object, Object> httpRouteStateConvert(
+      MethodHandle create, MethodHandle getUpdatedBySourceOrder, MethodHandle getRoute) {
+    return httpRouteHolder -> {
+      try {
+        int updatedBySourceOrder = (int) getUpdatedBySourceOrder.invoke(httpRouteHolder);
+        String route = (String) getRoute.invoke(httpRouteHolder);
+        return create.invoke(updatedBySourceOrder, route);
+      } catch (Throwable e) {
+        return null;
+      }
+    };
+  }
+
+  private InstrumentationApiContextBridging() {}
+}

+ 0 - 44
instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextBridgeTest.groovy

@@ -9,8 +9,6 @@ import io.opentelemetry.api.trace.Span
 import io.opentelemetry.context.Context
 import io.opentelemetry.context.ContextKey
 import io.opentelemetry.extension.annotations.WithSpan
-import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan
-import io.opentelemetry.instrumentation.api.internal.SpanKey
 import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
 
 import java.util.concurrent.CountDownLatch
@@ -148,48 +146,6 @@ class ContextBridgeTest extends AgentInstrumentationSpecification {
     Context.current() == Context.root()
   }
 
-  def "test local root span bridge"() {
-    expect:
-    AgentSpanTesting.runWithServerSpan("server") {
-      assert Span.current() != null
-      assert LocalRootSpan.fromContextOrNull(Context.current()) != null
-      runWithSpan("internal") {
-        assert LocalRootSpan.fromContextOrNull(Context.current()) != null
-      }
-    }
-  }
-
-  def "test span key bridge"() {
-    expect:
-    AgentSpanTesting.runWithAllSpanKeys("parent") {
-      assert Span.current() != null
-      def spanKeys = [
-        // span kind keys
-        SpanKey.KIND_SERVER,
-        SpanKey.KIND_CLIENT,
-        SpanKey.KIND_CONSUMER,
-        SpanKey.KIND_PRODUCER,
-        // semantic convention keys
-        SpanKey.HTTP_SERVER,
-        SpanKey.RPC_SERVER,
-        SpanKey.HTTP_CLIENT,
-        SpanKey.RPC_CLIENT,
-        SpanKey.DB_CLIENT,
-        SpanKey.PRODUCER,
-        SpanKey.CONSUMER_RECEIVE,
-        SpanKey.CONSUMER_PROCESS,
-      ]
-      spanKeys.each { spanKey ->
-        assert spanKey.fromContextOrNull(Context.current()) != null
-      }
-      runWithSpan("internal") {
-        spanKeys.each { spanKey ->
-          assert spanKey.fromContextOrNull(Context.current()) != null
-        }
-      }
-    }
-  }
-
   // TODO (trask)
   // more tests are needed here, not sure how to implement, probably need to write some test
   // instrumentation to help test, similar to :testing-common:integration-tests

+ 0 - 119
instrumentation/opentelemetry-api/opentelemetry-api-1.0/testing/src/main/java/AgentSpanTestingInstrumenter.java

@@ -1,119 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import io.opentelemetry.api.GlobalOpenTelemetry;
-import io.opentelemetry.api.common.AttributesBuilder;
-import io.opentelemetry.api.trace.Span;
-import io.opentelemetry.api.trace.SpanKind;
-import io.opentelemetry.context.Context;
-import io.opentelemetry.context.ContextKey;
-import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
-import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
-import io.opentelemetry.instrumentation.api.internal.SpanKey;
-import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider;
-import javax.annotation.Nullable;
-
-public final class AgentSpanTestingInstrumenter {
-
-  private static final ContextKey<Request> REQUEST_CONTEXT_KEY =
-      ContextKey.named("test-request-key");
-
-  private static final Instrumenter<Request, Void> INSTRUMENTER =
-      Instrumenter.<Request, Void>builder(
-              GlobalOpenTelemetry.get(), "test", request -> request.name)
-          .addContextCustomizer(
-              (context, request, startAttributes) -> context.with(REQUEST_CONTEXT_KEY, request))
-          .newInstrumenter(request -> request.kind);
-
-  private static final Instrumenter<Request, Void> INSTRUMENTER_HTTP_SERVER =
-      Instrumenter.<Request, Void>builder(
-              GlobalOpenTelemetry.get(), "test", request -> request.name)
-          .addAttributesExtractor(HttpServerSpanKeyAttributesExtractor.INSTANCE)
-          .addContextCustomizer(
-              (context, request, startAttributes) -> context.with(REQUEST_CONTEXT_KEY, request))
-          .newInstrumenter(request -> request.kind);
-
-  public static Context startHttpServerSpan(String name) {
-    return INSTRUMENTER_HTTP_SERVER.start(Context.current(), new Request(name, SpanKind.SERVER));
-  }
-
-  public static Context startClientSpan(String name) {
-    return start(name, SpanKind.CLIENT);
-  }
-
-  public static Context startSpanWithAllKeys(String name) {
-    Context context = start(name, SpanKind.INTERNAL);
-    Span span = Span.fromContext(context);
-    for (SpanKey spanKey : getSpanKeys()) {
-      context = spanKey.storeInContext(context, span);
-    }
-    return context;
-  }
-
-  private static Context start(String name, SpanKind kind) {
-    return INSTRUMENTER.start(Context.current(), new Request(name, kind));
-  }
-
-  public static void end(Context context, Throwable error) {
-    INSTRUMENTER.end(context, context.get(REQUEST_CONTEXT_KEY), null, error);
-  }
-
-  public static void endHttpServer(Context context, Throwable error) {
-    INSTRUMENTER_HTTP_SERVER.end(context, context.get(REQUEST_CONTEXT_KEY), null, error);
-  }
-
-  private static final class Request {
-    private final String name;
-    private final SpanKind kind;
-
-    public Request(String name, SpanKind kind) {
-      this.name = name;
-      this.kind = kind;
-    }
-  }
-
-  private static SpanKey[] getSpanKeys() {
-    return new SpanKey[] {
-      // span kind keys
-      SpanKey.KIND_SERVER,
-      SpanKey.KIND_CLIENT,
-      SpanKey.KIND_CONSUMER,
-      SpanKey.KIND_PRODUCER,
-      // semantic convention keys
-      SpanKey.HTTP_SERVER,
-      SpanKey.RPC_SERVER,
-      SpanKey.HTTP_CLIENT,
-      SpanKey.RPC_CLIENT,
-      SpanKey.DB_CLIENT,
-      SpanKey.PRODUCER,
-      SpanKey.CONSUMER_RECEIVE,
-      SpanKey.CONSUMER_PROCESS,
-    };
-  }
-
-  // simulate a real HTTP server implementation
-  private enum HttpServerSpanKeyAttributesExtractor
-      implements AttributesExtractor<Request, Void>, SpanKeyProvider {
-    INSTANCE;
-
-    @Override
-    public void onStart(AttributesBuilder attributes, Context parentContext, Request request) {}
-
-    @Override
-    public void onEnd(
-        AttributesBuilder attributes,
-        Context context,
-        Request request,
-        @Nullable Void unused,
-        @Nullable Throwable error) {}
-
-    @Override
-    public SpanKey internalGetSpanKey() {
-      return SpanKey.HTTP_SERVER;
-    }
-  }
-
-  private AgentSpanTestingInstrumenter() {}
-}

+ 45 - 0
instrumentation/opentelemetry-instrumentation-api/javaagent/build.gradle.kts

@@ -0,0 +1,45 @@
+plugins {
+  id("otel.javaagent-instrumentation")
+}
+
+dependencies {
+  implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:javaagent"))
+
+  compileOnly(project(":opentelemetry-api-shaded-for-instrumenting", configuration = "shadow"))
+
+  testImplementation(project(":instrumentation-api-semconv"))
+  testImplementation(project(":instrumentation:opentelemetry-instrumentation-api:testing"))
+  testInstrumentation(project(":instrumentation:opentelemetry-instrumentation-api:testing"))
+}
+
+testing {
+  suites {
+    val testOldServerSpan by registering(JvmTestSuite::class) {
+      dependencies {
+        implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv")
+        implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api")
+        implementation(project(":instrumentation:opentelemetry-instrumentation-api:testing"))
+      }
+    }
+  }
+}
+
+configurations.configureEach {
+  if (name.startsWith("testOldServerSpan")) {
+    resolutionStrategy {
+      dependencySubstitution {
+        // version 1.13.0 contains the old ServerSpan implementation that uses SERVER_KEY context key
+        substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv"))
+          .using(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv:1.13.0-alpha"))
+        substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api"))
+          .using(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:1.13.0-alpha"))
+      }
+    }
+  }
+}
+
+tasks {
+  check {
+    dependsOn(testing.suites)
+  }
+}

+ 57 - 0
instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/HttpRouteStateInstrumentation.java

@@ -0,0 +1,57 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.instrumentationapi;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+
+import application.io.opentelemetry.context.Context;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context.AgentContextStorage;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+final class HttpRouteStateInstrumentation implements TypeInstrumentation {
+
+  @Override
+  public ElementMatcher<TypeDescription> typeMatcher() {
+    return named("application.io.opentelemetry.instrumentation.api.internal.HttpRouteState");
+  }
+
+  @Override
+  public void transform(TypeTransformer transformer) {
+    transformer.applyAdviceToMethod(
+        named("update")
+            .and(takesArgument(0, named("application.io.opentelemetry.context.Context")))
+            .and(takesArgument(1, int.class))
+            .and(takesArgument(2, String.class)),
+        this.getClass().getName() + "$UpdateAdvice");
+  }
+
+  @SuppressWarnings("unused")
+  public static class UpdateAdvice {
+    @Advice.OnMethodEnter(suppress = Throwable.class)
+    public static void onEnter(
+        @Advice.Argument(0) Context applicationContext,
+        @Advice.Argument(1) int updatedBySourceOrder,
+        @Advice.Argument(2) String route) {
+
+      io.opentelemetry.context.Context agentContext =
+          AgentContextStorage.getAgentContext(applicationContext);
+
+      io.opentelemetry.instrumentation.api.internal.HttpRouteState agentRouteState =
+          io.opentelemetry.instrumentation.api.internal.HttpRouteState.fromContextOrNull(
+              agentContext);
+      if (agentRouteState == null) {
+        return;
+      }
+
+      agentRouteState.update(agentContext, updatedBySourceOrder, route);
+    }
+  }
+}

+ 34 - 0
instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/InstrumentationApiInstrumentationModule.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.instrumentationapi;
+
+import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
+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 java.util.List;
+import net.bytebuddy.matcher.ElementMatcher;
+
+@AutoService(InstrumentationModule.class)
+public class InstrumentationApiInstrumentationModule extends InstrumentationModule {
+
+  public InstrumentationApiInstrumentationModule() {
+    super("opentelemetry-instrumentation-api");
+  }
+
+  @Override
+  public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
+    return hasClassesNamed(
+        "application.io.opentelemetry.instrumentation.api.internal.HttpRouteState");
+  }
+
+  @Override
+  public List<TypeInstrumentation> typeInstrumentations() {
+    return singletonList(new HttpRouteStateInstrumentation());
+  }
+}

+ 115 - 0
instrumentation/opentelemetry-instrumentation-api/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/ContextBridgeTest.java

@@ -0,0 +1,115 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.instrumentationapi;
+
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource;
+import io.opentelemetry.instrumentation.api.internal.SpanKey;
+import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import io.opentelemetry.javaagent.instrumentation.testing.AgentSpanTesting;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class ContextBridgeTest {
+
+  @RegisterExtension
+  static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
+
+  @Test
+  void testLocalRootSpanBridge() {
+    AgentSpanTesting.runWithHttpServerSpan(
+        "server",
+        () -> {
+          assertNotNull(Span.fromContextOrNull(Context.current()));
+          assertNotNull(LocalRootSpan.fromContextOrNull(Context.current()));
+          testing.runWithSpan(
+              "internal", () -> assertNotNull(LocalRootSpan.fromContextOrNull(Context.current())));
+        });
+  }
+
+  @Test
+  void testSpanKeyBridge() {
+    AgentSpanTesting.runWithAllSpanKeys(
+        "parent",
+        () -> {
+          assertNotNull(Span.fromContextOrNull(Context.current()));
+
+          List<SpanKey> spanKeys =
+              Arrays.asList(
+                  // span kind keys
+                  SpanKey.KIND_SERVER,
+                  SpanKey.KIND_CLIENT,
+                  SpanKey.KIND_CONSUMER,
+                  SpanKey.KIND_PRODUCER,
+                  // semantic convention keys
+                  SpanKey.HTTP_SERVER,
+                  SpanKey.RPC_SERVER,
+                  SpanKey.HTTP_CLIENT,
+                  SpanKey.RPC_CLIENT,
+                  SpanKey.DB_CLIENT,
+                  SpanKey.PRODUCER,
+                  SpanKey.CONSUMER_RECEIVE,
+                  SpanKey.CONSUMER_PROCESS);
+
+          spanKeys.forEach(spanKey -> assertNotNull(spanKey.fromContextOrNull(Context.current())));
+
+          testing.runWithSpan(
+              "internal",
+              () ->
+                  spanKeys.forEach(
+                      spanKey -> assertNotNull(spanKey.fromContextOrNull(Context.current()))));
+        });
+  }
+
+  @Test
+  void testHttpRouteHolder_SameSourceAsServerInstrumentationDoesNotOverrideRoute() {
+    AgentSpanTesting.runWithHttpServerSpan(
+        "server",
+        () ->
+            HttpRouteHolder.updateHttpRoute(
+                Context.current(), HttpRouteSource.SERVLET, "/test/controller/:id"));
+
+    testing.waitAndAssertTraces(
+        trace ->
+            trace.hasSpansSatisfyingExactly(
+                span ->
+                    span.hasName("/test/server/*")
+                        .hasKind(SpanKind.SERVER)
+                        .hasNoParent()
+                        .hasAttributesSatisfyingExactly(
+                            equalTo(SemanticAttributes.HTTP_ROUTE, "/test/server/*"))));
+  }
+
+  @Test
+  void testHttpRouteHolder_SourceWithHigherOrderValueOverridesRoute() {
+    AgentSpanTesting.runWithHttpServerSpan(
+        "server",
+        () ->
+            HttpRouteHolder.updateHttpRoute(
+                Context.current(), HttpRouteSource.CONTROLLER, "/test/controller/:id"));
+
+    testing.waitAndAssertTraces(
+        trace ->
+            trace.hasSpansSatisfyingExactly(
+                span ->
+                    span.hasName("/test/controller/:id")
+                        .hasKind(SpanKind.SERVER)
+                        .hasNoParent()
+                        .hasAttributesSatisfyingExactly(
+                            equalTo(SemanticAttributes.HTTP_ROUTE, "/test/controller/:id"))));
+  }
+}

+ 4 - 1
instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/testOldInstrumentationApi/java/LegacyServerSpanContextBridgeTest.java → instrumentation/opentelemetry-instrumentation-api/javaagent/src/testOldServerSpan/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/LegacyServerSpanContextBridgeTest.java

@@ -3,6 +3,8 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
+package io.opentelemetry.javaagent.instrumentation.instrumentationapi;
+
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import io.opentelemetry.api.GlobalOpenTelemetry;
@@ -11,6 +13,7 @@ import io.opentelemetry.api.trace.Tracer;
 import io.opentelemetry.context.Context;
 import io.opentelemetry.context.Scope;
 import io.opentelemetry.instrumentation.api.server.ServerSpan;
+import io.opentelemetry.javaagent.instrumentation.testing.AgentSpanTesting;
 import org.junit.jupiter.api.Test;
 
 class LegacyServerSpanContextBridgeTest {
@@ -21,7 +24,7 @@ class LegacyServerSpanContextBridgeTest {
 
   @Test
   void shouldBridgeLegacyServerSpanClass() {
-    AgentSpanTesting.runWithServerSpan(
+    AgentSpanTesting.runWithHttpServerSpan(
         "server",
         () -> {
           assertNotNull(Span.current());

+ 0 - 0
instrumentation/opentelemetry-api/opentelemetry-api-1.0/testing/build.gradle.kts → instrumentation/opentelemetry-instrumentation-api/testing/build.gradle.kts


+ 3 - 9
instrumentation/opentelemetry-api/opentelemetry-api-1.0/testing/src/main/java/AgentSpanTesting.java → instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTesting.java

@@ -3,6 +3,8 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
+package io.opentelemetry.javaagent.instrumentation.testing;
+
 import io.opentelemetry.instrumentation.api.internal.SpanKey;
 
 public class AgentSpanTesting {
@@ -11,15 +13,7 @@ public class AgentSpanTesting {
    * Runs the provided {@code runnable} inside the scope of an SERVER span with name {@code
    * spanName}.
    */
-  public static void runWithServerSpan(String spanName, Runnable runnable) {
-    runnable.run();
-  }
-
-  /**
-   * Runs the provided {@code runnable} inside the scope of an CLIENT span with name {@code
-   * spanName}.
-   */
-  public static void runWithClientSpan(String spanName, Runnable runnable) {
+  public static void runWithHttpServerSpan(String spanName, Runnable runnable) {
     runnable.run();
   }
 

+ 5 - 27
instrumentation/opentelemetry-api/opentelemetry-api-1.0/testing/src/main/java/AgentSpanTestingInstrumentation.java → instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTestingInstrumentation.java

@@ -3,6 +3,8 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
+package io.opentelemetry.javaagent.instrumentation.testing;
+
 import static net.bytebuddy.matcher.ElementMatchers.named;
 
 import io.opentelemetry.context.Context;
@@ -17,21 +19,19 @@ public class AgentSpanTestingInstrumentation implements TypeInstrumentation {
 
   @Override
   public ElementMatcher<TypeDescription> typeMatcher() {
-    return named("AgentSpanTesting");
+    return named("io.opentelemetry.javaagent.instrumentation.testing.AgentSpanTesting");
   }
 
   @Override
   public void transform(TypeTransformer transformer) {
     transformer.applyAdviceToMethod(
-        named("runWithServerSpan"), this.getClass().getName() + "$RunWithServerSpanAdvice");
-    transformer.applyAdviceToMethod(
-        named("runWithClientSpan"), this.getClass().getName() + "$RunWithClientSpanAdvice");
+        named("runWithHttpServerSpan"), this.getClass().getName() + "$RunWithHttpServerSpanAdvice");
     transformer.applyAdviceToMethod(
         named("runWithAllSpanKeys"), this.getClass().getName() + "$RunWithAllSpanKeysAdvice");
   }
 
   @SuppressWarnings("unused")
-  public static class RunWithServerSpanAdvice {
+  public static class RunWithHttpServerSpanAdvice {
 
     @Advice.OnMethodEnter(suppress = Throwable.class)
     public static void onEnter(
@@ -52,28 +52,6 @@ public class AgentSpanTestingInstrumentation implements TypeInstrumentation {
     }
   }
 
-  @SuppressWarnings("unused")
-  public static class RunWithClientSpanAdvice {
-
-    @Advice.OnMethodEnter(suppress = Throwable.class)
-    public static void onEnter(
-        @Advice.Argument(0) String spanName,
-        @Advice.Local("otelContext") Context context,
-        @Advice.Local("otelScope") Scope scope) {
-      context = AgentSpanTestingInstrumenter.startClientSpan(spanName);
-      scope = context.makeCurrent();
-    }
-
-    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
-    public static void onExit(
-        @Advice.Thrown Throwable throwable,
-        @Advice.Local("otelContext") Context context,
-        @Advice.Local("otelScope") Scope scope) {
-      scope.close();
-      AgentSpanTestingInstrumenter.end(context, throwable);
-    }
-  }
-
   @SuppressWarnings("unused")
   public static class RunWithAllSpanKeysAdvice {
 

+ 3 - 6
instrumentation/opentelemetry-api/opentelemetry-api-1.0/testing/src/main/java/AgentSpanTestingInstrumentationModule.java → instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTestingInstrumentationModule.java

@@ -3,6 +3,8 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
+package io.opentelemetry.javaagent.instrumentation.testing;
+
 import static java.util.Collections.singletonList;
 
 import com.google.auto.service.AutoService;
@@ -13,12 +15,7 @@ import java.util.List;
 @AutoService(InstrumentationModule.class)
 public class AgentSpanTestingInstrumentationModule extends InstrumentationModule {
   public AgentSpanTestingInstrumentationModule() {
-    super(AgentSpanTestingInstrumentationModule.class.getName());
-  }
-
-  @Override
-  public boolean isHelperClass(String className) {
-    return className.startsWith("AgentSpanTestingInstrumenter");
+    super("agent-span-testing");
   }
 
   @Override

+ 82 - 0
instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTestingInstrumenter.java

@@ -0,0 +1,82 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.testing;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.ContextKey;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor;
+import io.opentelemetry.instrumentation.api.internal.SpanKey;
+
+public final class AgentSpanTestingInstrumenter {
+
+  private static final ContextKey<String> REQUEST_CONTEXT_KEY =
+      ContextKey.named("test-request-key");
+
+  private static final Instrumenter<String, Void> INSTRUMENTER =
+      Instrumenter.<String, Void>builder(GlobalOpenTelemetry.get(), "test", request -> request)
+          .addContextCustomizer(
+              (context, request, startAttributes) -> context.with(REQUEST_CONTEXT_KEY, request))
+          .newInstrumenter(SpanKindExtractor.alwaysInternal());
+
+  private static final Instrumenter<String, Void> HTTP_SERVER_INSTRUMENTER =
+      Instrumenter.<String, Void>builder(GlobalOpenTelemetry.get(), "test", request -> request)
+          .addAttributesExtractor(
+              HttpServerAttributesExtractor.create(MockHttpServerAttributesGetter.INSTANCE))
+          .addContextCustomizer(HttpRouteHolder.get())
+          .addContextCustomizer(
+              (context, request, startAttributes) -> context.with(REQUEST_CONTEXT_KEY, request))
+          .newInstrumenter(SpanKindExtractor.alwaysServer());
+
+  public static Context startHttpServerSpan(String name) {
+    Context context = HTTP_SERVER_INSTRUMENTER.start(Context.current(), name);
+    HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.SERVLET, "/test/server/*");
+    return context;
+  }
+
+  public static void endHttpServer(Context context, Throwable error) {
+    HTTP_SERVER_INSTRUMENTER.end(context, context.get(REQUEST_CONTEXT_KEY), null, error);
+  }
+
+  public static Context startSpanWithAllKeys(String name) {
+    Context context = INSTRUMENTER.start(Context.current(), name);
+    Span span = Span.fromContext(context);
+    for (SpanKey spanKey : getAllSpanKeys()) {
+      context = spanKey.storeInContext(context, span);
+    }
+    return context;
+  }
+
+  public static void end(Context context, Throwable error) {
+    INSTRUMENTER.end(context, context.get(REQUEST_CONTEXT_KEY), null, error);
+  }
+
+  private static SpanKey[] getAllSpanKeys() {
+    return new SpanKey[] {
+      // span kind keys
+      SpanKey.KIND_SERVER,
+      SpanKey.KIND_CLIENT,
+      SpanKey.KIND_CONSUMER,
+      SpanKey.KIND_PRODUCER,
+      // semantic convention keys
+      SpanKey.HTTP_SERVER,
+      SpanKey.RPC_SERVER,
+      SpanKey.HTTP_CLIENT,
+      SpanKey.RPC_CLIENT,
+      SpanKey.DB_CLIENT,
+      SpanKey.PRODUCER,
+      SpanKey.CONSUMER_RECEIVE,
+      SpanKey.CONSUMER_PROCESS,
+    };
+  }
+
+  private AgentSpanTestingInstrumenter() {}
+}

+ 94 - 0
instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/MockHttpServerAttributesGetter.java

@@ -0,0 +1,94 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.testing;
+
+import static java.util.Collections.emptyList;
+
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter;
+import java.util.List;
+import javax.annotation.Nullable;
+
+// only needed so that HttpServerAttributesExtractor can be added to the HTTP server instrumenter,
+// and http.route is properly set
+enum MockHttpServerAttributesGetter implements HttpServerAttributesGetter<String, Void> {
+  INSTANCE;
+
+  @Nullable
+  @Override
+  public String method(String s) {
+    return null;
+  }
+
+  @Override
+  public List<String> requestHeader(String s, String name) {
+    return emptyList();
+  }
+
+  @Nullable
+  @Override
+  public Long requestContentLength(String s, @Nullable Void unused) {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public Long requestContentLengthUncompressed(String s, @Nullable Void unused) {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public Integer statusCode(String s, Void unused) {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public Long responseContentLength(String s, Void unused) {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public Long responseContentLengthUncompressed(String s, Void unused) {
+    return null;
+  }
+
+  @Override
+  public List<String> responseHeader(String s, Void unused, String name) {
+    return emptyList();
+  }
+
+  @Nullable
+  @Override
+  public String flavor(String s) {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String target(String s) {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String route(String s) {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String scheme(String s) {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String serverName(String s) {
+    return null;
+  }
+}

+ 2 - 1
settings.gradle.kts

@@ -330,9 +330,10 @@ include(":instrumentation:okhttp:okhttp-3.0:testing")
 include(":instrumentation:opentelemetry-annotations-1.0:javaagent")
 include(":instrumentation:opentelemetry-annotations-1.0:testing")
 include(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:javaagent")
-include(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:testing")
 include(":instrumentation:opentelemetry-api:opentelemetry-api-1.4:javaagent")
 include(":instrumentation:opentelemetry-api:opentelemetry-api-1.10:javaagent")
+include(":instrumentation:opentelemetry-instrumentation-api:javaagent")
+include(":instrumentation:opentelemetry-instrumentation-api:testing")
 include(":instrumentation:oshi:javaagent")
 include(":instrumentation:oshi:library")
 include(":instrumentation:oshi:testing")