diff --git a/conventions/src/main/kotlin/otel.java-conventions.gradle.kts b/conventions/src/main/kotlin/otel.java-conventions.gradle.kts
index 77675111108c..1a0c5249a4f1 100644
--- a/conventions/src/main/kotlin/otel.java-conventions.gradle.kts
+++ b/conventions/src/main/kotlin/otel.java-conventions.gradle.kts
@@ -469,6 +469,7 @@ configurations.configureEach {
       substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api")).using(project(":instrumentation-api"))
       substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator")).using(project(":instrumentation-api-incubator"))
       substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations")).using(project(":instrumentation-annotations"))
+      substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations-incubator")).using(project(":instrumentation-annotations-incubator"))
       substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations-support")).using(
         project(":instrumentation-annotations-support")
       )
diff --git a/instrumentation-annotations-incubator/build.gradle.kts b/instrumentation-annotations-incubator/build.gradle.kts
new file mode 100644
index 000000000000..321ea11d7f85
--- /dev/null
+++ b/instrumentation-annotations-incubator/build.gradle.kts
@@ -0,0 +1,13 @@
+plugins {
+  id("otel.java-conventions")
+  id("otel.japicmp-conventions")
+  id("otel.publish-conventions")
+
+  id("otel.animalsniffer-conventions")
+}
+
+group = "io.opentelemetry.instrumentation"
+
+dependencies {
+  api("io.opentelemetry:opentelemetry-api")
+}
diff --git a/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/Counted.java b/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/Counted.java
new file mode 100644
index 000000000000..dd4957068f1d
--- /dev/null
+++ b/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/Counted.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.annotations.incubator;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation creates a {@link io.opentelemetry.api.metrics.LongCounter Counter} instrument
+ * recording the number of invocations of the annotated method or constructor.
+ *
+ * <p>By default, the Counter instrument will have the following attributes:
+ *
+ * <ul>
+ *   <li><b>code.namespace:</b> The fully qualified name of the class whose method is invoked.
+ *   <li><b>code.function:</b> The name of the annotated method.
+ *   <li><b>error.type:</b> This is only present if an Exception is thrown, and contains the {@link
+ *       Class#getName name} of the Exception class.
+ * </ul>
+ *
+ * <p>Application developers can use this annotation to signal OpenTelemetry auto-instrumentation
+ * that the Counter instrument should be created.
+ *
+ * <p>If you are a library developer, then probably you should NOT use this annotation, because it
+ * is non-functional without the OpenTelemetry auto-instrumentation agent, or some other annotation
+ * processor.
+ */
+@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Counted {
+
+  /**
+   * Name of the Counter instrument.
+   *
+   * <p>The name should follow the instrument naming rule: <a
+   * href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-naming-rule">https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-naming-rule</a>
+   */
+  String value();
+
+  /**
+   * Description of the instrument.
+   *
+   * <p>Description strings should follow the instrument description rules: <a
+   * href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-description">https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-description</a>
+   *
+   * <p>This property would not take effect if the value is not specified.
+   */
+  String description() default "";
+
+  /**
+   * Unit of the instrument.
+   *
+   * <p>Unit strings should follow the instrument unit rules: <a
+   * href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-unit">https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-unit</a>
+   *
+   * <p>This property would not take effect if the value is not specified.
+   */
+  String unit() default "{invocation}";
+}
diff --git a/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/MetricAttribute.java b/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/MetricAttribute.java
new file mode 100644
index 000000000000..d12ca1770eb1
--- /dev/null
+++ b/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/MetricAttribute.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.annotations.incubator;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation marks that a parameter of a method or constructor annotated with {@link Timed} or
+ * {@link Counted} should be added as an attribute to the instrument.
+ *
+ * <p>Application developers can use this annotation to signal OpenTelemetry auto-instrumentation
+ * that the attribute should be created.
+ *
+ * <p>If you are a library developer, then probably you should NOT use this annotation, because it
+ * is non-functional without the OpenTelemetry auto-instrumentation agent, or some other annotation
+ * processor.
+ *
+ * <p>Warning: be careful using this because it might cause an explosion of the cardinality on your
+ * metric.
+ */
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface MetricAttribute {
+
+  /**
+   * Optional name of the attribute.
+   *
+   * <p>If not specified and the code is compiled using the `{@code -parameters}` argument to
+   * `javac`, the parameter name will be used instead. If the parameter name is not available, e.g.,
+   * because the code was not compiled with that flag, the attribute will be ignored.
+   */
+  String value() default "";
+}
diff --git a/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/MetricAttributeForReturnValue.java b/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/MetricAttributeForReturnValue.java
new file mode 100644
index 000000000000..fbb6de40c52e
--- /dev/null
+++ b/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/MetricAttributeForReturnValue.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.annotations.incubator;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation allows for adding method return value as attribute to the metrics recorded using
+ * {@link Timed} and {@link Counted} annotations.
+ *
+ * <p>Application developers can use this annotation to signal OpenTelemetry auto-instrumentation
+ * that the attribute should be created.
+ *
+ * <p>If you are a library developer, then probably you should NOT use this annotation, because it
+ * is non-functional without the OpenTelemetry auto-instrumentation agent, or some other annotation
+ * processor.
+ *
+ * <p>Warning: be careful using this because it might cause an explosion of the cardinality on your
+ * metric.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface MetricAttributeForReturnValue {
+
+  /**
+   * Attribute name for the return value.
+   *
+   * <p>The name of the attribute for the return value of the method call. {@link Object#toString()}
+   * may be called on the return value to convert it to a String.
+   */
+  String value();
+}
diff --git a/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/StaticMetricAttribute.java b/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/StaticMetricAttribute.java
new file mode 100644
index 000000000000..b65881016374
--- /dev/null
+++ b/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/StaticMetricAttribute.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.annotations.incubator;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation allows for adding attributes to the metrics recorded using {@link Timed} and
+ * {@link Counted} annotations.
+ *
+ * <p>Application developers can use this annotation to signal OpenTelemetry auto-instrumentation
+ * that the attribute should be created.
+ *
+ * <p>If you are a library developer, then probably you should NOT use this annotation, because it
+ * is non-functional without the OpenTelemetry auto-instrumentation agent, or some other annotation
+ * processor.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Repeatable(StaticMetricAttributes.class)
+public @interface StaticMetricAttribute {
+
+  /** Name of the attribute. */
+  String name();
+
+  /** Value of the attribute. */
+  String value();
+}
diff --git a/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/StaticMetricAttributes.java b/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/StaticMetricAttributes.java
new file mode 100644
index 000000000000..904c3daf81b7
--- /dev/null
+++ b/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/StaticMetricAttributes.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.annotations.incubator;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation allows for adding attributes to the metrics recorded using {@link Timed} and
+ * {@link Counted} annotations.
+ *
+ * <p>Application developers can use this annotation to signal OpenTelemetry auto-instrumentation
+ * that the attribute should be created.
+ *
+ * <p>If you are a library developer, then probably you should NOT use this annotation, because it
+ * is non-functional without the OpenTelemetry auto-instrumentation agent, or some other annotation
+ * processor.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface StaticMetricAttributes {
+
+  /** Array of {@link StaticMetricAttribute} annotations describing the added attributes. */
+  StaticMetricAttribute[] value();
+}
diff --git a/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/Timed.java b/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/Timed.java
new file mode 100644
index 000000000000..c461a8f6f82f
--- /dev/null
+++ b/instrumentation-annotations-incubator/src/main/java/io/opentelemetry/instrumentation/annotations/incubator/Timed.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.annotations.incubator;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This annotation creates a {@link io.opentelemetry.api.metrics.DoubleHistogram Histogram}
+ * instrument observing the duration of invocations of the annotated method or constructor.
+ *
+ * <p>By default, the Histogram instrument will have the following attributes:
+ *
+ * <ul>
+ *   <li><b>code.namespace:</b> The fully qualified name of the class whose method is invoked.
+ *   <li><b>code.function:</b> The name of the annotated method.
+ *   <li><b>error.type:</b> This is only present if an Exception is thrown, and contains the {@link
+ *       Class#getName name} of the Exception class.
+ * </ul>
+ *
+ * <p>Application developers can use this annotation to signal OpenTelemetry auto-instrumentation
+ * that the Histogram instrument should be created.
+ *
+ * <p>If you are a library developer, then probably you should NOT use this annotation, because it
+ * is non-functional without the OpenTelemetry auto-instrumentation agent, or some other annotation
+ * processor.
+ */
+@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Timed {
+
+  /**
+   * Name of the Histogram instrument.
+   *
+   * <p>The name should follow the instrument naming rule: <a
+   * href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-naming-rule">https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-naming-rule</a>
+   */
+  String value();
+
+  /**
+   * Description for the instrument.
+   *
+   * <p>Description strings should follow the instrument description rules: <a
+   * href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-description">https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-description</a>
+   */
+  String description() default "";
+
+  /**
+   * The unit for the instrument.
+   *
+   * <p>Default is seconds.
+   */
+  TimeUnit unit() default TimeUnit.SECONDS;
+}
diff --git a/instrumentation-annotations-incubator/src/test/java/io/opentelemetry/instrumentation/annotations/incubator/CountedUsageExamples.java b/instrumentation-annotations-incubator/src/test/java/io/opentelemetry/instrumentation/annotations/incubator/CountedUsageExamples.java
new file mode 100644
index 000000000000..0cd9adf68120
--- /dev/null
+++ b/instrumentation-annotations-incubator/src/test/java/io/opentelemetry/instrumentation/annotations/incubator/CountedUsageExamples.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.annotations.incubator;
+
+public class CountedUsageExamples {
+
+  @Counted("customizedName")
+  public void method() {}
+
+  @Counted("methodWithAttributes")
+  public void attributes(
+      @MetricAttribute String attribute1, @MetricAttribute("attribute2") long attribute2) {}
+}
diff --git a/instrumentation-annotations-incubator/src/test/java/io/opentelemetry/instrumentation/annotations/incubator/TimedUsageExamples.java b/instrumentation-annotations-incubator/src/test/java/io/opentelemetry/instrumentation/annotations/incubator/TimedUsageExamples.java
new file mode 100644
index 000000000000..793c55f8de4b
--- /dev/null
+++ b/instrumentation-annotations-incubator/src/test/java/io/opentelemetry/instrumentation/annotations/incubator/TimedUsageExamples.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.annotations.incubator;
+
+public class TimedUsageExamples {
+
+  @Timed("customizedName")
+  public void method() {}
+
+  @Timed("methodWithAttributes")
+  public void attributes(
+      @MetricAttribute String attribute1, @MetricAttribute("attribute2") long attribute2) {}
+}
diff --git a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/MethodBinder.java b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/MethodBinder.java
new file mode 100644
index 000000000000..0413b3298feb
--- /dev/null
+++ b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/MethodBinder.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.annotation.support;
+
+import io.opentelemetry.api.common.AttributesBuilder;
+import java.lang.reflect.Method;
+import java.util.function.BiConsumer;
+import javax.annotation.Nullable;
+
+/** Helper class for binding method parameters and return value to attributes. */
+public final class MethodBinder {
+
+  /** Create binding for method return value. */
+  @Nullable
+  public static BiConsumer<AttributesBuilder, Object> bindReturnValue(
+      Method method, String attributeName) {
+    Class<?> returnType = method.getReturnType();
+    if (returnType == void.class) {
+      return null;
+    }
+    AttributeBinding binding = AttributeBindingFactory.createBinding(attributeName, returnType);
+    return binding::apply;
+  }
+
+  /** Create binding for method parameters. */
+  @Nullable
+  public static BiConsumer<AttributesBuilder, Object[]> bindParameters(
+      Method method, ParameterAttributeNamesExtractor parameterAttributeNamesExtractor) {
+    AttributeBindings bindings = AttributeBindings.bind(method, parameterAttributeNamesExtractor);
+    if (bindings.isEmpty()) {
+      return null;
+    }
+    return bindings::apply;
+  }
+
+  private MethodBinder() {}
+}
diff --git a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/async/AsyncOperationEndHandler.java b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/async/AsyncOperationEndHandler.java
new file mode 100644
index 000000000000..0d82aa16aa8b
--- /dev/null
+++ b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/async/AsyncOperationEndHandler.java
@@ -0,0 +1,15 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.annotation.support.async;
+
+import io.opentelemetry.context.Context;
+import javax.annotation.Nullable;
+
+/** Callback that is called when async computation completes. */
+public interface AsyncOperationEndHandler<REQUEST, RESPONSE> {
+  void handle(
+      Context context, REQUEST request, @Nullable RESPONSE response, @Nullable Throwable error);
+}
diff --git a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/async/AsyncOperationEndStrategy.java b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/async/AsyncOperationEndStrategy.java
index 01dbaa55eef5..d3bba94fbe6c 100644
--- a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/async/AsyncOperationEndStrategy.java
+++ b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/async/AsyncOperationEndStrategy.java
@@ -11,7 +11,8 @@
 /**
  * Implementations of this interface describe how to compose over {@linkplain #supports(Class)
  * supported} asynchronous computation types and delay marking the operation as ended by calling
- * {@link Instrumenter#end(Context, Object, Object, Throwable)}.
+ * {@link Instrumenter#end(Context, Object, Object, Throwable)} or {@link
+ * AsyncOperationEndHandler#handle(Context, Object, Object, Throwable)}.
  */
 public interface AsyncOperationEndStrategy {
 
@@ -36,10 +37,35 @@ public interface AsyncOperationEndStrategy {
    * @return Either {@code asyncValue} or a value composing over {@code asyncValue} for notification
    *     of completion.
    */
-  <REQUEST, RESPONSE> Object end(
+  default <REQUEST, RESPONSE> Object end(
       Instrumenter<REQUEST, RESPONSE> instrumenter,
       Context context,
       REQUEST request,
       Object asyncValue,
+      Class<RESPONSE> responseType) {
+    return end(instrumenter::end, context, request, asyncValue, responseType);
+  }
+
+  /**
+   * Composes over {@code asyncValue} and delays the {@link AsyncOperationEndHandler#handle(Context,
+   * Object, Object, Throwable)} call until after the asynchronous operation represented by {@code
+   * asyncValue} completes.
+   *
+   * @param handler The {@link AsyncOperationEndHandler} to be used to end the operation stored in
+   *     the {@code context}.
+   * @param asyncValue Return value from the instrumented method. Must be an instance of a {@code
+   *     asyncType} for which {@link #supports(Class)} returned true (in particular it must not be
+   *     {@code null}).
+   * @param responseType Expected type of the response that should be obtained from the {@code
+   *     asyncValue}. If the result of the async computation is instance of the passed type it will
+   *     be passed when the {@code handler} is called.
+   * @return Either {@code asyncValue} or a value composing over {@code asyncValue} for notification
+   *     of completion.
+   */
+  <REQUEST, RESPONSE> Object end(
+      AsyncOperationEndHandler<REQUEST, RESPONSE> handler,
+      Context context,
+      REQUEST request,
+      Object asyncValue,
       Class<RESPONSE> responseType);
 }
diff --git a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/async/AsyncOperationEndSupport.java b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/async/AsyncOperationEndSupport.java
index 8bcbd5c3f978..50edfb7ab051 100644
--- a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/async/AsyncOperationEndSupport.java
+++ b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/async/AsyncOperationEndSupport.java
@@ -10,40 +10,59 @@
 import javax.annotation.Nullable;
 
 /**
- * A wrapper over {@link Instrumenter} that is able to defer {@link Instrumenter#end(Context,
- * Object, Object, Throwable)} until asynchronous computation finishes.
+ * A wrapper over {@link AsyncOperationEndHandler} that is able to defer {@link
+ * AsyncOperationEndHandler#handle(Context, Object, Object, Throwable)} until asynchronous
+ * computation finishes.
  */
 public final class AsyncOperationEndSupport<REQUEST, RESPONSE> {
 
   /**
-   * Returns a new {@link AsyncOperationEndSupport} that wraps over passed {@code syncInstrumenter},
+   * Returns a new {@link AsyncOperationEndSupport} that wraps over passed {@code instrumenter},
    * configured for usage with asynchronous computations that are instances of {@code asyncType}. If
    * the result of the async computation ends up being an instance of {@code responseType} it will
-   * be passed as the response to the {@code syncInstrumenter} call; otherwise {@code null} value
-   * will be used as the response.
+   * be passed as the response to the {@code instrumenter} call; otherwise {@code null} value will
+   * be used as the response.
    */
   public static <REQUEST, RESPONSE> AsyncOperationEndSupport<REQUEST, RESPONSE> create(
-      Instrumenter<REQUEST, RESPONSE> syncInstrumenter,
+      Instrumenter<REQUEST, RESPONSE> instrumenter,
       Class<RESPONSE> responseType,
       Class<?> asyncType) {
     return new AsyncOperationEndSupport<>(
-        syncInstrumenter,
+        instrumenter::end,
         responseType,
         asyncType,
         AsyncOperationEndStrategies.instance().resolveStrategy(asyncType));
   }
 
-  private final Instrumenter<REQUEST, RESPONSE> instrumenter;
+  /**
+   * Returns a new {@link AsyncOperationEndSupport} that wraps over passed {@code handler},
+   * configured for usage with asynchronous computations that are instances of {@code asyncType}. If
+   * the result of the async computation ends up being an instance of {@code responseType} it will
+   * be passed as the response to the {@code handler} call; otherwise {@code null} value will be
+   * used as the response.
+   */
+  public static <REQUEST, RESPONSE> AsyncOperationEndSupport<REQUEST, RESPONSE> create(
+      AsyncOperationEndHandler<REQUEST, RESPONSE> handler,
+      Class<RESPONSE> responseType,
+      Class<?> asyncType) {
+    return new AsyncOperationEndSupport<>(
+        handler,
+        responseType,
+        asyncType,
+        AsyncOperationEndStrategies.instance().resolveStrategy(asyncType));
+  }
+
+  private final AsyncOperationEndHandler<REQUEST, RESPONSE> handler;
   private final Class<RESPONSE> responseType;
   private final Class<?> asyncType;
   @Nullable private final AsyncOperationEndStrategy asyncOperationEndStrategy;
 
   private AsyncOperationEndSupport(
-      Instrumenter<REQUEST, RESPONSE> instrumenter,
+      AsyncOperationEndHandler<REQUEST, RESPONSE> handler,
       Class<RESPONSE> responseType,
       Class<?> asyncType,
       @Nullable AsyncOperationEndStrategy asyncOperationEndStrategy) {
-    this.instrumenter = instrumenter;
+    this.handler = handler;
     this.responseType = responseType;
     this.asyncType = asyncType;
     this.asyncOperationEndStrategy = asyncOperationEndStrategy;
@@ -68,18 +87,18 @@ public <ASYNC> ASYNC asyncEnd(
       Context context, REQUEST request, @Nullable ASYNC asyncValue, @Nullable Throwable throwable) {
     // we can end early if an exception was thrown
     if (throwable != null) {
-      instrumenter.end(context, request, null, throwable);
+      handler.handle(context, request, null, throwable);
       return asyncValue;
     }
 
     // use the configured strategy to compose over the asyncValue
     if (asyncOperationEndStrategy != null && asyncType.isInstance(asyncValue)) {
       return (ASYNC)
-          asyncOperationEndStrategy.end(instrumenter, context, request, asyncValue, responseType);
+          asyncOperationEndStrategy.end(handler, context, request, asyncValue, responseType);
     }
 
     // fall back to sync end() if asyncValue type doesn't match
-    instrumenter.end(context, request, tryToGetResponse(responseType, asyncValue), null);
+    handler.handle(context, request, tryToGetResponse(responseType, asyncValue), null);
     return asyncValue;
   }
 
diff --git a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/async/Jdk8AsyncOperationEndStrategy.java b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/async/Jdk8AsyncOperationEndStrategy.java
index c1eea3a419ed..38498bbbe894 100644
--- a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/async/Jdk8AsyncOperationEndStrategy.java
+++ b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/async/Jdk8AsyncOperationEndStrategy.java
@@ -8,7 +8,6 @@
 import static io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndSupport.tryToGetResponse;
 
 import io.opentelemetry.context.Context;
-import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 
@@ -22,20 +21,20 @@ public boolean supports(Class<?> asyncType) {
 
   @Override
   public <REQUEST, RESPONSE> Object end(
-      Instrumenter<REQUEST, RESPONSE> instrumenter,
+      AsyncOperationEndHandler<REQUEST, RESPONSE> handler,
       Context context,
       REQUEST request,
       Object asyncValue,
       Class<RESPONSE> responseType) {
     if (asyncValue instanceof CompletableFuture) {
       CompletableFuture<?> future = (CompletableFuture<?>) asyncValue;
-      if (tryToEndSynchronously(instrumenter, context, request, future, responseType)) {
+      if (tryToEndSynchronously(handler, context, request, future, responseType)) {
         return future;
       }
-      return endWhenComplete(instrumenter, context, request, future, responseType);
+      return endWhenComplete(handler, context, request, future, responseType);
     }
     CompletionStage<?> stage = (CompletionStage<?>) asyncValue;
-    return endWhenComplete(instrumenter, context, request, stage, responseType);
+    return endWhenComplete(handler, context, request, stage, responseType);
   }
 
   /**
@@ -44,7 +43,7 @@ public <REQUEST, RESPONSE> Object end(
    * notification of completion.
    */
   private static <REQUEST, RESPONSE> boolean tryToEndSynchronously(
-      Instrumenter<REQUEST, RESPONSE> instrumenter,
+      AsyncOperationEndHandler<REQUEST, RESPONSE> handler,
       Context context,
       REQUEST request,
       CompletableFuture<?> future,
@@ -56,9 +55,9 @@ private static <REQUEST, RESPONSE> boolean tryToEndSynchronously(
 
     try {
       Object potentialResponse = future.join();
-      instrumenter.end(context, request, tryToGetResponse(responseType, potentialResponse), null);
+      handler.handle(context, request, tryToGetResponse(responseType, potentialResponse), null);
     } catch (Throwable t) {
-      instrumenter.end(context, request, null, t);
+      handler.handle(context, request, null, t);
     }
     return true;
   }
@@ -68,13 +67,13 @@ private static <REQUEST, RESPONSE> boolean tryToEndSynchronously(
    * span will be ended.
    */
   private static <REQUEST, RESPONSE> CompletionStage<?> endWhenComplete(
-      Instrumenter<REQUEST, RESPONSE> instrumenter,
+      AsyncOperationEndHandler<REQUEST, RESPONSE> handler,
       Context context,
       REQUEST request,
       CompletionStage<?> stage,
       Class<RESPONSE> responseType) {
     return stage.whenComplete(
         (result, exception) ->
-            instrumenter.end(context, request, tryToGetResponse(responseType, result), exception));
+            handler.handle(context, request, tryToGetResponse(responseType, result), exception));
   }
 }
diff --git a/instrumentation/guava-10.0/library/src/main/java/io/opentelemetry/instrumentation/guava/v10_0/GuavaAsyncOperationEndStrategy.java b/instrumentation/guava-10.0/library/src/main/java/io/opentelemetry/instrumentation/guava/v10_0/GuavaAsyncOperationEndStrategy.java
index dbdbff784728..24287d362804 100644
--- a/instrumentation/guava-10.0/library/src/main/java/io/opentelemetry/instrumentation/guava/v10_0/GuavaAsyncOperationEndStrategy.java
+++ b/instrumentation/guava-10.0/library/src/main/java/io/opentelemetry/instrumentation/guava/v10_0/GuavaAsyncOperationEndStrategy.java
@@ -12,8 +12,8 @@
 import io.opentelemetry.api.common.AttributeKey;
 import io.opentelemetry.api.trace.Span;
 import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndHandler;
 import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndStrategy;
-import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
 
 public final class GuavaAsyncOperationEndStrategy implements AsyncOperationEndStrategy {
   // Visible for testing
@@ -41,19 +41,19 @@ public boolean supports(Class<?> returnType) {
 
   @Override
   public <REQUEST, RESPONSE> Object end(
-      Instrumenter<REQUEST, RESPONSE> instrumenter,
+      AsyncOperationEndHandler<REQUEST, RESPONSE> handler,
       Context context,
       REQUEST request,
       Object asyncValue,
       Class<RESPONSE> responseType) {
 
     ListenableFuture<?> future = (ListenableFuture<?>) asyncValue;
-    end(instrumenter, context, request, future, responseType);
+    end(handler, context, request, future, responseType);
     return future;
   }
 
   private <REQUEST, RESPONSE> void end(
-      Instrumenter<REQUEST, RESPONSE> instrumenter,
+      AsyncOperationEndHandler<REQUEST, RESPONSE> handler,
       Context context,
       REQUEST request,
       ListenableFuture<?> future,
@@ -63,18 +63,17 @@ private <REQUEST, RESPONSE> void end(
         if (captureExperimentalSpanAttributes) {
           Span.fromContext(context).setAttribute(CANCELED_ATTRIBUTE_KEY, true);
         }
-        instrumenter.end(context, request, null, null);
+        handler.handle(context, request, null, null);
       } else {
         try {
           Object response = Uninterruptibles.getUninterruptibly(future);
-          instrumenter.end(context, request, tryToGetResponse(responseType, response), null);
+          handler.handle(context, request, tryToGetResponse(responseType, response), null);
         } catch (Throwable exception) {
-          instrumenter.end(context, request, null, exception);
+          handler.handle(context, request, null, exception);
         }
       }
     } else {
-      future.addListener(
-          () -> end(instrumenter, context, request, future, responseType), Runnable::run);
+      future.addListener(() -> end(handler, context, request, future, responseType), Runnable::run);
     }
   }
 }
diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/build.gradle.kts b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/build.gradle.kts
index 3e1be05c41ff..8d937d750112 100644
--- a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/build.gradle.kts
+++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/build.gradle.kts
@@ -30,7 +30,7 @@ dependencies {
 
   implementation("org.ow2.asm:asm-tree")
   implementation("org.ow2.asm:asm-util")
-  implementation(project(":instrumentation:opentelemetry-instrumentation-annotations-1.16:javaagent"))
+  implementation(project(":instrumentation:opentelemetry-instrumentation-annotations:opentelemetry-instrumentation-annotations-common:javaagent"))
 
   testInstrumentation(project(":instrumentation:opentelemetry-extension-kotlin-1.0:javaagent"))
   testInstrumentation(project(":instrumentation:reactor:reactor-3.1:javaagent"))
diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/build.gradle.kts b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/build.gradle.kts
index 755a2a9fa85f..1aa3ece053e8 100644
--- a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/build.gradle.kts
+++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/build.gradle.kts
@@ -12,6 +12,7 @@ dependencies {
   compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0")
   compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
   compileOnly(project(":instrumentation-api"))
+  compileOnly(project(":instrumentation-annotations-support"))
 }
 
 kotlin {
diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/src/main/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowUtil.kt b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/src/main/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowUtil.kt
index 402251782cba..dbd541182d86 100644
--- a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/src/main/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowUtil.kt
+++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/src/main/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowUtil.kt
@@ -6,10 +6,10 @@
 package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.flow
 
 import io.opentelemetry.context.Context
-import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
+import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndHandler
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.onCompletion
 
-fun <REQUEST, RESPONSE> onComplete(flow: Flow<*>, instrumenter: Instrumenter<REQUEST, RESPONSE>, context: Context, request: REQUEST & Any): Flow<*> = flow.onCompletion { cause: Throwable? ->
-  instrumenter.end(context, request, null, cause)
+fun <REQUEST, RESPONSE> onComplete(flow: Flow<*>, handler: AsyncOperationEndHandler<REQUEST, RESPONSE>, context: Context, request: REQUEST & Any): Flow<*> = flow.onCompletion { cause: Throwable? ->
+  handler.handle(context, request, null, cause)
 }
diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowInstrumentationHelper.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowInstrumentationHelper.java
index 02102afe01d0..0f0206d8c9fc 100644
--- a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowInstrumentationHelper.java
+++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowInstrumentationHelper.java
@@ -6,9 +6,9 @@
 package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.flow;
 
 import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndHandler;
 import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndStrategies;
 import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndStrategy;
-import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
 import kotlinx.coroutines.flow.Flow;
 
 public final class FlowInstrumentationHelper {
@@ -32,13 +32,13 @@ public boolean supports(Class<?> returnType) {
 
     @Override
     public <REQUEST, RESPONSE> Object end(
-        Instrumenter<REQUEST, RESPONSE> instrumenter,
+        AsyncOperationEndHandler<REQUEST, RESPONSE> handler,
         Context context,
         REQUEST request,
         Object asyncValue,
         Class<RESPONSE> responseType) {
       Flow<?> flow = (Flow<?>) asyncValue;
-      return FlowUtilKt.onComplete(flow, instrumenter, context, request);
+      return FlowUtilKt.onComplete(flow, handler, context, request);
     }
   }
 }
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/README.md b/instrumentation/opentelemetry-instrumentation-annotations/README.md
similarity index 100%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/README.md
rename to instrumentation/opentelemetry-instrumentation-annotations/README.md
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/build.gradle.kts b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/build.gradle.kts
similarity index 91%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/build.gradle.kts
rename to instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/build.gradle.kts
index 6ea1548e4354..e9f6b111a94c 100644
--- a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/build.gradle.kts
+++ b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/build.gradle.kts
@@ -16,8 +16,8 @@ muzzle {
 
 dependencies {
   compileOnly(project(":instrumentation-annotations-support"))
-
   compileOnly(project(":javaagent-tooling"))
+  implementation(project(":instrumentation:opentelemetry-instrumentation-annotations:opentelemetry-instrumentation-annotations-common:javaagent"))
 
   // this instrumentation needs to do similar shading dance as opentelemetry-api-1.0 because
   // the @WithSpan annotation references the OpenTelemetry API's SpanKind class
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AddingSpanAttributesInstrumentation.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AddingSpanAttributesInstrumentation.java
similarity index 100%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AddingSpanAttributesInstrumentation.java
rename to instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AddingSpanAttributesInstrumentation.java
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationInstrumentationModule.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationInstrumentationModule.java
similarity index 100%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationInstrumentationModule.java
rename to instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationInstrumentationModule.java
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationSingletons.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationSingletons.java
similarity index 100%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationSingletons.java
rename to instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationSingletons.java
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodCodeAttributesGetter.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodCodeAttributesGetter.java
similarity index 100%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodCodeAttributesGetter.java
rename to instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodCodeAttributesGetter.java
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodRequestCodeAttributesGetter.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodRequestCodeAttributesGetter.java
similarity index 100%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodRequestCodeAttributesGetter.java
rename to instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodRequestCodeAttributesGetter.java
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentation.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentation.java
similarity index 100%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentation.java
rename to instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentation.java
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanParameterAttributeNamesExtractor.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanParameterAttributeNamesExtractor.java
similarity index 100%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanParameterAttributeNamesExtractor.java
rename to instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanParameterAttributeNamesExtractor.java
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/AddingSpanAttributesInstrumentationTest.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/AddingSpanAttributesInstrumentationTest.java
similarity index 100%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/AddingSpanAttributesInstrumentationTest.java
rename to instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/AddingSpanAttributesInstrumentationTest.java
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/ExtractAttributesUsingAddingSpanAttributes.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/ExtractAttributesUsingAddingSpanAttributes.java
similarity index 100%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/ExtractAttributesUsingAddingSpanAttributes.java
rename to instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/ExtractAttributesUsingAddingSpanAttributes.java
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/TracedWithSpan.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/TracedWithSpan.java
similarity index 100%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/TracedWithSpan.java
rename to instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/TracedWithSpan.java
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java
similarity index 100%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java
rename to instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java
diff --git a/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-common/javaagent/build.gradle.kts b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-common/javaagent/build.gradle.kts
new file mode 100644
index 000000000000..8c365ac278a8
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-common/javaagent/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+  id("otel.javaagent-instrumentation")
+}
+
+dependencies {
+  compileOnly(project(":instrumentation-annotations-support"))
+
+  compileOnly(project(":javaagent-tooling"))
+}
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationExcludedMethods.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationExcludedMethods.java
similarity index 100%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationExcludedMethods.java
rename to instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationExcludedMethods.java
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/KotlinCoroutineUtil.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/KotlinCoroutineUtil.java
similarity index 100%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/KotlinCoroutineUtil.java
rename to instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/KotlinCoroutineUtil.java
diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodRequest.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodRequest.java
similarity index 100%
rename from instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodRequest.java
rename to instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodRequest.java
diff --git a/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/build.gradle.kts b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/build.gradle.kts
new file mode 100644
index 000000000000..f5e3d05ae1f4
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/build.gradle.kts
@@ -0,0 +1,41 @@
+plugins {
+  id("otel.javaagent-instrumentation")
+}
+
+// note that muzzle is not run against the current SNAPSHOT instrumentation-annotations, but this is
+// ok because the tests are run against the current SNAPSHOT instrumentation-annotations which will
+// catch any muzzle issues in SNAPSHOT instrumentation-annotations
+
+muzzle {
+  pass {
+    group.set("io.opentelemetry")
+    module.set("opentelemetry-instrumentation-annotations-incubator")
+    versions.set("(,)")
+  }
+}
+
+dependencies {
+  compileOnly(project(":instrumentation-annotations-support"))
+  compileOnly(project(":javaagent-tooling"))
+  implementation(project(":instrumentation:opentelemetry-instrumentation-annotations:opentelemetry-instrumentation-annotations-common:javaagent"))
+
+  // this instrumentation needs to do similar shading dance as opentelemetry-api-1.0 because
+  // the @WithSpan annotation references the OpenTelemetry API's SpanKind class
+  //
+  // see the comment in opentelemetry-api-1.0.gradle for more details
+  compileOnly(project(":opentelemetry-instrumentation-annotations-shaded-for-instrumenting", configuration = "shadow"))
+
+  testImplementation(project(":instrumentation-annotations-incubator"))
+  testImplementation(project(":instrumentation-annotations-support"))
+}
+
+tasks {
+  compileTestJava {
+    options.compilerArgs.add("-parameters")
+  }
+  test {
+    jvmArgs(
+      "-Dotel.instrumentation.opentelemetry-instrumentation-annotations.exclude-methods=io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator.counted.CountedExample[exampleIgnore];io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator.timed.TimedExample[exampleIgnore]"
+    )
+  }
+}
diff --git a/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/CountedHelper.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/CountedHelper.java
new file mode 100644
index 000000000000..c54187c6c534
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/CountedHelper.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator;
+
+import application.io.opentelemetry.instrumentation.annotations.incubator.Counted;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndSupport;
+import io.opentelemetry.javaagent.instrumentation.instrumentationannotations.MethodRequest;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public final class CountedHelper extends MetricsAnnotationHelper {
+
+  private static final ClassValue<Map<Method, MethodCounter>> counters =
+      new ClassValue<Map<Method, MethodCounter>>() {
+        @Override
+        protected Map<Method, MethodCounter> computeValue(Class<?> type) {
+          return new ConcurrentHashMap<>();
+        }
+      };
+
+  public static Object recordWithAttributes(
+      MethodRequest methodRequest, Object returnValue, Throwable throwable) {
+    return record(methodRequest.method(), returnValue, throwable, methodRequest.args());
+  }
+
+  public static Object record(Method method, Object returnValue, Throwable throwable) {
+    return record(method, returnValue, throwable, null);
+  }
+
+  private static Object record(
+      Method method, Object returnValue, Throwable throwable, Object[] arguments) {
+    AsyncOperationEndSupport<Method, Object> operationEndSupport =
+        AsyncOperationEndSupport.create(
+            (context, m, object, error) -> getMethodCounter(m).record(object, arguments, error),
+            Object.class,
+            method.getReturnType());
+    return operationEndSupport.asyncEnd(Context.current(), method, returnValue, throwable);
+  }
+
+  private static MethodCounter getMethodCounter(Method method) {
+    return counters.get(method.getDeclaringClass()).computeIfAbsent(method, MethodCounter::new);
+  }
+
+  private static class MethodCounter {
+    private final LongCounter counter;
+    private final MetricAttributeHelper attributeHelper;
+
+    MethodCounter(Method method) {
+      Counted countedAnnotation = method.getAnnotation(Counted.class);
+      counter =
+          METER
+              .counterBuilder(countedAnnotation.value())
+              .setDescription(countedAnnotation.description())
+              .setUnit(countedAnnotation.unit())
+              .build();
+      attributeHelper = new MetricAttributeHelper(method);
+    }
+
+    void record(Object returnValue, Object[] arguments, Throwable throwable) {
+      counter.add(1, attributeHelper.getAttributes(returnValue, arguments, throwable));
+    }
+  }
+
+  private CountedHelper() {}
+}
diff --git a/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/CountedInstrumentation.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/CountedInstrumentation.java
new file mode 100644
index 000000000000..d18e6283f162
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/CountedInstrumentation.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator;
+
+import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.KotlinCoroutineUtil.isKotlinSuspendMethod;
+import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
+import static net.bytebuddy.matcher.ElementMatchers.hasParameters;
+import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.not;
+import static net.bytebuddy.matcher.ElementMatchers.whereAny;
+
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import io.opentelemetry.javaagent.instrumentation.instrumentationannotations.AnnotationExcludedMethods;
+import io.opentelemetry.javaagent.instrumentation.instrumentationannotations.MethodRequest;
+import java.lang.reflect.Method;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.annotation.AnnotationSource;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.implementation.bytecode.assign.Assigner;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class CountedInstrumentation implements TypeInstrumentation {
+
+  private final ElementMatcher.Junction<AnnotationSource> annotatedMethodMatcher;
+  private final ElementMatcher.Junction<MethodDescription> annotatedParametersMatcher;
+  // this matcher matches all methods that should be excluded from transformation
+  private final ElementMatcher.Junction<MethodDescription> excludedMethodsMatcher;
+
+  CountedInstrumentation() {
+    annotatedMethodMatcher =
+        isAnnotatedWith(
+            named("application.io.opentelemetry.instrumentation.annotations.incubator.Counted"));
+    annotatedParametersMatcher =
+        hasParameters(
+            whereAny(
+                isAnnotatedWith(
+                    named(
+                        "application.io.opentelemetry.instrumentation.annotations.incubator.MetricAttribute"))));
+    // exclude all kotlin suspend methods, these are handle in kotlinx-coroutines instrumentation
+    excludedMethodsMatcher =
+        AnnotationExcludedMethods.configureExcludedMethods().or(isKotlinSuspendMethod());
+  }
+
+  @Override
+  public ElementMatcher<TypeDescription> typeMatcher() {
+    return declaresMethod(annotatedMethodMatcher);
+  }
+
+  @Override
+  public void transform(TypeTransformer transformer) {
+    ElementMatcher.Junction<MethodDescription> countedMethods =
+        annotatedMethodMatcher.and(not(excludedMethodsMatcher));
+
+    ElementMatcher.Junction<MethodDescription> timedMethodsWithParameters =
+        countedMethods.and(annotatedParametersMatcher);
+
+    ElementMatcher.Junction<MethodDescription> timedMethodsWithoutParameters =
+        countedMethods.and(not(annotatedParametersMatcher));
+
+    transformer.applyAdviceToMethod(
+        timedMethodsWithoutParameters, CountedInstrumentation.class.getName() + "$CountedAdvice");
+
+    // Only apply advice for tracing parameters as attributes if any of the parameters are annotated
+    // with @MetricsAttribute to avoid unnecessarily copying the arguments into an array.
+    transformer.applyAdviceToMethod(
+        timedMethodsWithParameters,
+        CountedInstrumentation.class.getName() + "$CountedAttributesAdvice");
+  }
+
+  @SuppressWarnings("unused")
+  public static class CountedAttributesAdvice {
+
+    @Advice.OnMethodEnter(suppress = Throwable.class)
+    public static void onEnter(
+        @Advice.Origin Method method,
+        @Advice.AllArguments(typing = Assigner.Typing.DYNAMIC) Object[] args,
+        @Advice.Local("otelRequest") MethodRequest request) {
+      // Every usage of @Advice.Origin Method is replaced with a call to Class.getMethod, copy it
+      // to local variable so that there would be only one call to Class.getMethod.
+      request = new MethodRequest(method, args);
+    }
+
+    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+    public static void onExit(
+        @Advice.Local("otelRequest") MethodRequest request,
+        @Advice.Return(typing = Assigner.Typing.DYNAMIC, readOnly = false) Object returnValue,
+        @Advice.Thrown Throwable throwable) {
+      returnValue = CountedHelper.recordWithAttributes(request, returnValue, throwable);
+    }
+  }
+
+  @SuppressWarnings("unused")
+  public static class CountedAdvice {
+
+    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+    public static void onExit(
+        @Advice.Origin Method method,
+        @Advice.Return(typing = Assigner.Typing.DYNAMIC, readOnly = false) Object returnValue,
+        @Advice.Thrown Throwable throwable) {
+      returnValue = CountedHelper.record(method, returnValue, throwable);
+    }
+  }
+}
diff --git a/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/MetricsAnnotationHelper.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/MetricsAnnotationHelper.java
new file mode 100644
index 000000000000..9885d92dd789
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/MetricsAnnotationHelper.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator;
+
+import application.io.opentelemetry.instrumentation.annotations.incubator.MetricAttribute;
+import application.io.opentelemetry.instrumentation.annotations.incubator.MetricAttributeForReturnValue;
+import application.io.opentelemetry.instrumentation.annotations.incubator.StaticMetricAttribute;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.api.metrics.Meter;
+import io.opentelemetry.instrumentation.api.annotation.support.MethodBinder;
+import io.opentelemetry.instrumentation.api.annotation.support.ParameterAttributeNamesExtractor;
+import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.function.BiConsumer;
+import javax.annotation.Nullable;
+
+public abstract class MetricsAnnotationHelper {
+  private static final String INSTRUMENTATION_NAME =
+      "io.opentelemetry.opentelemetry-instrumentation-annotations-incubator";
+  static final Meter METER = GlobalOpenTelemetry.get().getMeter(INSTRUMENTATION_NAME);
+  static final ParameterAttributeNamesExtractor PARAMETER_ATTRIBUTE_NAMES_EXTRACTOR =
+      (method, parameters) -> {
+        String[] attributeNames = new String[parameters.length];
+        for (int i = 0; i < parameters.length; i++) {
+          attributeNames[i] = attributeName(parameters[i]);
+        }
+        return attributeNames;
+      };
+
+  static void addStaticAttributes(Method method, AttributesBuilder attributesBuilder) {
+    attributesBuilder.put(
+        CodeIncubatingAttributes.CODE_NAMESPACE, method.getDeclaringClass().getName());
+    attributesBuilder.put(CodeIncubatingAttributes.CODE_FUNCTION_NAME, method.getName());
+
+    StaticMetricAttribute[] staticMetricAttributes =
+        method.getDeclaredAnnotationsByType(StaticMetricAttribute.class);
+    for (StaticMetricAttribute staticMetricAttribute : staticMetricAttributes) {
+      attributesBuilder.put(staticMetricAttribute.name(), staticMetricAttribute.value());
+    }
+  }
+
+  @Nullable
+  private static String attributeName(Parameter parameter) {
+    MetricAttribute annotation = parameter.getDeclaredAnnotation(MetricAttribute.class);
+    if (annotation == null) {
+      return null;
+    }
+    String value = annotation.value();
+    if (!value.isEmpty()) {
+      return value;
+    } else if (parameter.isNamePresent()) {
+      return parameter.getName();
+    } else {
+      return null;
+    }
+  }
+
+  static class MetricAttributeHelper {
+    private final BiConsumer<AttributesBuilder, Object[]> bindParameters;
+    private final BiConsumer<AttributesBuilder, Object> bindReturn;
+    private final Attributes staticAttributes;
+
+    MetricAttributeHelper(Method method) {
+      bindParameters = MethodBinder.bindParameters(method, PARAMETER_ATTRIBUTE_NAMES_EXTRACTOR);
+      MetricAttributeForReturnValue returnValueAttribute =
+          method.getAnnotation(MetricAttributeForReturnValue.class);
+      bindReturn =
+          returnValueAttribute != null
+              ? MethodBinder.bindReturnValue(method, returnValueAttribute.value())
+              : null;
+
+      AttributesBuilder attributesBuilder = Attributes.builder();
+      addStaticAttributes(method, attributesBuilder);
+      staticAttributes = attributesBuilder.build();
+    }
+
+    Attributes getAttributes(Object returnValue, Object[] arguments, Throwable throwable) {
+      AttributesBuilder attributesBuilder = Attributes.builder();
+      attributesBuilder.putAll(staticAttributes);
+      if (arguments != null && bindParameters != null) {
+        bindParameters.accept(attributesBuilder, arguments);
+      }
+      if (returnValue != null && bindReturn != null) {
+        bindReturn.accept(attributesBuilder, returnValue);
+      }
+      if (throwable != null) {
+        attributesBuilder.put("error.type", throwable.getClass().getName());
+      }
+      return attributesBuilder.build();
+    }
+  }
+}
diff --git a/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/MetricsAnnotationInstrumentationModule.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/MetricsAnnotationInstrumentationModule.java
new file mode 100644
index 000000000000..6c63097aa0cb
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/MetricsAnnotationInstrumentationModule.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator;
+
+import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
+import static java.util.Arrays.asList;
+
+import application.io.opentelemetry.instrumentation.annotations.incubator.Counted;
+import application.io.opentelemetry.instrumentation.annotations.incubator.MetricAttribute;
+import application.io.opentelemetry.instrumentation.annotations.incubator.Timed;
+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;
+
+/**
+ * Instrumentation for methods annotated with {@link Counted}, {@link Timed} and {@link
+ * MetricAttribute} annotations.
+ */
+@AutoService(InstrumentationModule.class)
+public class MetricsAnnotationInstrumentationModule extends InstrumentationModule {
+
+  public MetricsAnnotationInstrumentationModule() {
+    super("opentelemetry-instrumentation-annotations-incubator", "metrics-annotations");
+  }
+
+  @Override
+  public int order() {
+    // Run first to ensure other automatic instrumentation is added after and therefore is executed
+    // earlier in the instrumented method and create the span to attach attributes to.
+    return -1000;
+  }
+
+  @Override
+  public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
+    return hasClassesNamed(
+        "application.io.opentelemetry.instrumentation.annotations.incubator.Counted");
+  }
+
+  @Override
+  public boolean isIndyModule() {
+    // TimedInstrumentation does not work with indy
+    return false;
+  }
+
+  @Override
+  public List<TypeInstrumentation> typeInstrumentations() {
+    return asList(new CountedInstrumentation(), new TimedInstrumentation());
+  }
+}
diff --git a/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/TimedHelper.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/TimedHelper.java
new file mode 100644
index 000000000000..537d0e4efa1b
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/TimedHelper.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+import application.io.opentelemetry.instrumentation.annotations.incubator.Timed;
+import io.opentelemetry.api.metrics.DoubleHistogram;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndSupport;
+import io.opentelemetry.javaagent.instrumentation.instrumentationannotations.MethodRequest;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+public final class TimedHelper extends MetricsAnnotationHelper {
+
+  private static final ClassValue<Map<Method, MethodTimer>> timers =
+      new ClassValue<Map<Method, MethodTimer>>() {
+        @Override
+        protected Map<Method, MethodTimer> computeValue(Class<?> type) {
+          return new ConcurrentHashMap<>();
+        }
+      };
+
+  public static Object recordWithAttributes(
+      MethodRequest methodRequest, Object returnValue, Throwable throwable, long startNanoTime) {
+    return record(
+        methodRequest.method(), returnValue, throwable, startNanoTime, methodRequest.args());
+  }
+
+  public static Object record(
+      Method method, Object returnValue, Throwable throwable, long startNanoTime) {
+    return record(method, returnValue, throwable, startNanoTime, null);
+  }
+
+  private static Object record(
+      Method method,
+      Object returnValue,
+      Throwable throwable,
+      long startNanoTime,
+      Object[] arguments) {
+    AsyncOperationEndSupport<Method, Object> operationEndSupport =
+        AsyncOperationEndSupport.create(
+            (context, m, object, error) ->
+                getMethodTimer(m).record(object, arguments, error, startNanoTime),
+            Object.class,
+            method.getReturnType());
+    return operationEndSupport.asyncEnd(Context.current(), method, returnValue, throwable);
+  }
+
+  private static MethodTimer getMethodTimer(Method method) {
+    return timers.get(method.getDeclaringClass()).computeIfAbsent(method, MethodTimer::new);
+  }
+
+  private static String timeUnitToString(TimeUnit timeUnit) {
+    switch (timeUnit) {
+      case NANOSECONDS:
+        return "ns";
+      case MICROSECONDS:
+        return "us";
+      case MILLISECONDS:
+        return "ms";
+      case SECONDS:
+        return "s";
+      case MINUTES:
+        return "min";
+      case HOURS:
+        return "h";
+      case DAYS:
+        return "d";
+    }
+    throw new IllegalArgumentException("Unsupported time unit " + timeUnit);
+  }
+
+  private static double getDuration(long startNanoTime, TimeUnit unit) {
+    long nanoDelta = System.nanoTime() - startNanoTime;
+    return (double) nanoDelta / NANOSECONDS.convert(1, unit);
+  }
+
+  private static class MethodTimer {
+    private final TimeUnit unit;
+    private final DoubleHistogram histogram;
+    private final MetricAttributeHelper attributeHelper;
+
+    MethodTimer(Method method) {
+      Timed timedAnnotation = method.getAnnotation(Timed.class);
+      unit = timedAnnotation.unit();
+      histogram =
+          METER
+              .histogramBuilder(timedAnnotation.value())
+              .setDescription(timedAnnotation.description())
+              .setUnit(timeUnitToString(unit))
+              .build();
+      attributeHelper = new MetricAttributeHelper(method);
+    }
+
+    void record(Object returnValue, Object[] arguments, Throwable throwable, long startNanoTime) {
+      double duration = getDuration(startNanoTime, unit);
+      histogram.record(duration, attributeHelper.getAttributes(returnValue, arguments, throwable));
+    }
+  }
+
+  private TimedHelper() {}
+}
diff --git a/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/TimedInstrumentation.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/TimedInstrumentation.java
new file mode 100644
index 000000000000..94c31d0c2d70
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/TimedInstrumentation.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator;
+
+import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.KotlinCoroutineUtil.isKotlinSuspendMethod;
+import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
+import static net.bytebuddy.matcher.ElementMatchers.hasParameters;
+import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.not;
+import static net.bytebuddy.matcher.ElementMatchers.whereAny;
+
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import io.opentelemetry.javaagent.instrumentation.instrumentationannotations.AnnotationExcludedMethods;
+import io.opentelemetry.javaagent.instrumentation.instrumentationannotations.MethodRequest;
+import java.lang.reflect.Method;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.annotation.AnnotationSource;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.implementation.bytecode.assign.Assigner;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class TimedInstrumentation implements TypeInstrumentation {
+
+  private final ElementMatcher.Junction<AnnotationSource> annotatedMethodMatcher;
+  private final ElementMatcher.Junction<MethodDescription> annotatedParametersMatcher;
+  // this matcher matches all methods that should be excluded from transformation
+  private final ElementMatcher.Junction<MethodDescription> excludedMethodsMatcher;
+
+  TimedInstrumentation() {
+    annotatedMethodMatcher =
+        isAnnotatedWith(
+            named("application.io.opentelemetry.instrumentation.annotations.incubator.Timed"));
+    annotatedParametersMatcher =
+        hasParameters(
+            whereAny(
+                isAnnotatedWith(
+                    named(
+                        "application.io.opentelemetry.instrumentation.annotations.incubator.MetricAttribute"))));
+    // exclude all kotlin suspend methods, these are handle in kotlinx-coroutines instrumentation
+    excludedMethodsMatcher =
+        AnnotationExcludedMethods.configureExcludedMethods().or(isKotlinSuspendMethod());
+  }
+
+  @Override
+  public ElementMatcher<TypeDescription> typeMatcher() {
+    return declaresMethod(annotatedMethodMatcher);
+  }
+
+  @Override
+  public void transform(TypeTransformer transformer) {
+    ElementMatcher.Junction<MethodDescription> timedMethods =
+        annotatedMethodMatcher.and(not(excludedMethodsMatcher));
+
+    ElementMatcher.Junction<MethodDescription> timedMethodsWithParameters =
+        timedMethods.and(annotatedParametersMatcher);
+
+    ElementMatcher.Junction<MethodDescription> timedMethodsWithoutParameters =
+        timedMethods.and(not(annotatedParametersMatcher));
+
+    transformer.applyAdviceToMethod(
+        timedMethodsWithoutParameters, TimedInstrumentation.class.getName() + "$TimedAdvice");
+
+    // Only apply advice for tracing parameters as attributes if any of the parameters are annotated
+    // with @MetricsAttribute to avoid unnecessarily copying the arguments into an array.
+    transformer.applyAdviceToMethod(
+        timedMethodsWithParameters,
+        TimedInstrumentation.class.getName() + "$TimedAttributesAdvice");
+  }
+
+  @SuppressWarnings("unused")
+  public static class TimedAttributesAdvice {
+
+    @Advice.OnMethodEnter(suppress = Throwable.class)
+    public static void onEnter(
+        @Advice.Origin Method method,
+        @Advice.AllArguments(typing = Assigner.Typing.DYNAMIC) Object[] args,
+        @Advice.Local("otelRequest") MethodRequest request,
+        @Advice.Local("startNanoTime") long startNanoTime) {
+      // Every usage of @Advice.Origin Method is replaced with a call to Class.getMethod, copy it
+      // to local variable so that there would be only one call to Class.getMethod.
+      request = new MethodRequest(method, args);
+      startNanoTime = System.nanoTime();
+    }
+
+    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+    public static void onExit(
+        @Advice.Local("otelRequest") MethodRequest request,
+        @Advice.Local("startNanoTime") long startNanoTime,
+        @Advice.Return(typing = Assigner.Typing.DYNAMIC, readOnly = false) Object returnValue,
+        @Advice.Thrown Throwable throwable) {
+      returnValue =
+          TimedHelper.recordWithAttributes(request, returnValue, throwable, startNanoTime);
+    }
+  }
+
+  @SuppressWarnings("unused")
+  public static class TimedAdvice {
+
+    @Advice.OnMethodEnter(suppress = Throwable.class)
+    public static void onEnter(
+        @Advice.Origin Method originMethod,
+        @Advice.Local("otelMethod") Method method,
+        @Advice.Local("startNanoTime") long startNanoTime) {
+      // Every usage of @Advice.Origin Method is replaced with a call to Class.getMethod, copy it
+      // to local variable so that there would be only one call to Class.getMethod.
+      method = originMethod;
+      startNanoTime = System.nanoTime();
+    }
+
+    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+    public static void onExit(
+        @Advice.Local("otelMethod") Method method,
+        @Advice.Local("startNanoTime") long startNanoTime,
+        @Advice.Return(typing = Assigner.Typing.DYNAMIC, readOnly = false) Object returnValue,
+        @Advice.Thrown Throwable throwable) {
+      returnValue = TimedHelper.record(method, returnValue, throwable, startNanoTime);
+    }
+  }
+}
diff --git a/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/counted/CountedExample.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/counted/CountedExample.java
new file mode 100644
index 000000000000..915933c53b3c
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/counted/CountedExample.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator.counted;
+
+import io.opentelemetry.instrumentation.annotations.incubator.Counted;
+import io.opentelemetry.instrumentation.annotations.incubator.MetricAttributeForReturnValue;
+import io.opentelemetry.instrumentation.annotations.incubator.StaticMetricAttribute;
+import java.util.concurrent.CompletableFuture;
+
+public class CountedExample {
+
+  public static final String METRIC_NAME = "name.count";
+  public static final String METRIC_DESCRIPTION = "I am the description.";
+  public static final String METRIC_UNIT = "ms";
+  public static final String RETURN_STRING = "I am a return string.";
+
+  @Counted(METRIC_NAME)
+  public void exampleWithName() {}
+
+  @Counted(value = "example.with.description.count", description = METRIC_DESCRIPTION)
+  public void exampleWithDescription() {}
+
+  @Counted(value = "example.with.unit.count", unit = METRIC_UNIT)
+  public void exampleWithUnit() {}
+
+  @Counted("example.with.attributes.count")
+  @StaticMetricAttribute(name = "key1", value = "value1")
+  @StaticMetricAttribute(name = "key2", value = "value2")
+  @StaticMetricAttribute(name = "key2", value = "value2")
+  public void exampleWithAdditionalAttributes1() {}
+
+  @Counted(value = "example.with.return.count")
+  @MetricAttributeForReturnValue("returnValue")
+  public ReturnObject exampleWithReturnValueAttribute() {
+    return new ReturnObject();
+  }
+
+  @Counted("example.with.exception.count")
+  public void exampleWithException() {
+    throw new IllegalStateException("test exception.");
+  }
+
+  @Counted("example.ignore.count")
+  public void exampleIgnore() {}
+
+  @Counted(value = "example.completable.future.count")
+  @MetricAttributeForReturnValue("returnValue")
+  public CompletableFuture<String> completableFuture(CompletableFuture<String> future) {
+    return future;
+  }
+
+  public static class ReturnObject {
+    @Override
+    public String toString() {
+      return RETURN_STRING;
+    }
+  }
+}
diff --git a/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/counted/CountedInstrumentationTest.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/counted/CountedInstrumentationTest.java
new file mode 100644
index 000000000000..8c235605dda5
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/counted/CountedInstrumentationTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator.counted;
+
+import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator.counted.CountedExample.METRIC_DESCRIPTION;
+import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator.counted.CountedExample.METRIC_NAME;
+import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator.counted.CountedExample.METRIC_UNIT;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class CountedInstrumentationTest {
+
+  @RegisterExtension
+  private static final AgentInstrumentationExtension testing =
+      AgentInstrumentationExtension.create();
+
+  private static final String INSTRUMENTATION_NAME =
+      "io.opentelemetry.opentelemetry-instrumentation-annotations-incubator";
+
+  @Test
+  void testExampleWithAnotherName() {
+    new CountedExample().exampleWithName();
+    testing.waitAndAssertMetrics(INSTRUMENTATION_NAME, metric -> metric.hasName(METRIC_NAME));
+  }
+
+  @Test
+  void testExampleWithDescription() {
+    new CountedExample().exampleWithDescription();
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        metric ->
+            metric.hasName("example.with.description.count").hasDescription(METRIC_DESCRIPTION));
+  }
+
+  @Test
+  void testExampleWithUnit() {
+    new CountedExample().exampleWithUnit();
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        metric -> metric.hasName("example.with.unit.count").hasUnit(METRIC_UNIT));
+  }
+
+  @Test
+  void testExampleWithAdditionalAttributes1() {
+    new CountedExample().exampleWithAdditionalAttributes1();
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        metric ->
+            metric
+                .hasName("example.with.attributes.count")
+                .satisfies(
+                    metricData ->
+                        assertThat(metricData.getData().getPoints())
+                            .allMatch(
+                                p ->
+                                    "value1"
+                                            .equals(
+                                                p.getAttributes()
+                                                    .get(AttributeKey.stringKey("key1")))
+                                        && "value2"
+                                            .equals(
+                                                p.getAttributes()
+                                                    .get(AttributeKey.stringKey("key2"))))));
+  }
+
+  @Test
+  void testExampleWithReturnAttribute() {
+    new CountedExample().exampleWithReturnValueAttribute();
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        metric ->
+            metric
+                .hasName("example.with.return.count")
+                .satisfies(
+                    metricData ->
+                        assertThat(metricData.getData().getPoints())
+                            .allMatch(
+                                p ->
+                                    CountedExample.RETURN_STRING.equals(
+                                        p.getAttributes()
+                                            .get(AttributeKey.stringKey("returnValue"))))));
+  }
+
+  @Test
+  void testExampleWithException() {
+    try {
+      new CountedExample().exampleWithException();
+    } catch (IllegalStateException e) {
+      // noop
+    }
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        metric ->
+            metric
+                .hasName("example.with.exception.count")
+                .satisfies(
+                    metricData ->
+                        assertThat(metricData.getData().getPoints())
+                            .allMatch(
+                                p ->
+                                    IllegalStateException.class
+                                        .getName()
+                                        .equals(
+                                            p.getAttributes()
+                                                .get(AttributeKey.stringKey("error.type"))))));
+  }
+
+  @Test
+  void testExampleIgnore() throws Exception {
+    new CountedExample().exampleIgnore();
+    Thread.sleep(500); // sleep a bit just to make sure no metric is captured
+    assertThat(testing.metrics()).isEmpty();
+  }
+
+  @Test
+  void testCompletableFuture() throws Exception {
+    CompletableFuture<String> future = new CompletableFuture<>();
+    new CountedExample().completableFuture(future);
+
+    Thread.sleep(500); // sleep a bit just to make sure no metric is captured
+    assertThat(testing.metrics()).isEmpty();
+
+    future.complete("Done");
+
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        metric ->
+            metric
+                .hasName("example.completable.future.count")
+                .satisfies(
+                    metricData ->
+                        assertThat(metricData.getData().getPoints())
+                            .allMatch(
+                                p ->
+                                    "Done"
+                                        .equals(
+                                            p.getAttributes()
+                                                .get(AttributeKey.stringKey("returnValue"))))));
+  }
+}
diff --git a/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/timed/TimedExample.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/timed/TimedExample.java
new file mode 100644
index 000000000000..ae9f4ce93ef0
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/timed/TimedExample.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator.timed;
+
+import io.opentelemetry.instrumentation.annotations.incubator.MetricAttributeForReturnValue;
+import io.opentelemetry.instrumentation.annotations.incubator.StaticMetricAttribute;
+import io.opentelemetry.instrumentation.annotations.incubator.Timed;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+public class TimedExample {
+  public static final String METRIC_NAME = "name.duration";
+  public static final String METRIC_DESCRIPTION = "I am the description.";
+  public static final String RETURN_STRING = "I am a return string.";
+
+  @Timed(METRIC_NAME)
+  public void exampleWithName() {}
+
+  @Timed(value = "example.with.description.duration", description = METRIC_DESCRIPTION)
+  public void exampleWithDescription() {}
+
+  @Timed(value = "example.with.unit.duration", unit = TimeUnit.MILLISECONDS)
+  public void exampleWithUnit() throws InterruptedException {
+    Thread.sleep(2000);
+  }
+
+  @Timed("example.with.attributes.duration")
+  @StaticMetricAttribute(name = "key1", value = "value1")
+  @StaticMetricAttribute(name = "key2", value = "value2")
+  public void exampleWithAdditionalAttributes1() {}
+
+  @Timed("example.ignore.duration")
+  public void exampleIgnore() {}
+
+  @Timed("example.with.exception.duration")
+  public void exampleWithException() {
+    throw new IllegalStateException("test");
+  }
+
+  @Timed(value = "example.with.return.duration")
+  @MetricAttributeForReturnValue("returnValue")
+  public ReturnObject exampleWithReturnValueAttribute() {
+    return new ReturnObject();
+  }
+
+  @Timed(value = "example.completable.future.duration")
+  @MetricAttributeForReturnValue("returnValue")
+  public CompletableFuture<String> completableFuture(CompletableFuture<String> future) {
+    return future;
+  }
+
+  public static class ReturnObject {
+    @Override
+    public String toString() {
+      return RETURN_STRING;
+    }
+  }
+}
diff --git a/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/timed/TimedInstrumentationTest.java b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/timed/TimedInstrumentationTest.java
new file mode 100644
index 000000000000..cb6d72c9dc6d
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-annotations/opentelemetry-instrumentation-annotations-incubator/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/incubator/timed/TimedInstrumentationTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator.timed;
+
+import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator.timed.TimedExample.METRIC_DESCRIPTION;
+import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.incubator.timed.TimedExample.METRIC_NAME;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class TimedInstrumentationTest {
+
+  @RegisterExtension
+  private static final AgentInstrumentationExtension testing =
+      AgentInstrumentationExtension.create();
+
+  private static final String TIMED_INSTRUMENTATION_NAME =
+      "io.opentelemetry.opentelemetry-instrumentation-annotations-incubator";
+
+  @Test
+  void testExampleWithName() {
+    new TimedExample().exampleWithName();
+    testing.waitAndAssertMetrics(
+        TIMED_INSTRUMENTATION_NAME, metric -> metric.hasName(METRIC_NAME).hasUnit("s"));
+  }
+
+  @Test
+  void testExampleWithDescription() {
+    new TimedExample().exampleWithDescription();
+    testing.waitAndAssertMetrics(
+        TIMED_INSTRUMENTATION_NAME,
+        metric ->
+            metric.hasName("example.with.description.duration").hasDescription(METRIC_DESCRIPTION));
+  }
+
+  @Test
+  void testExampleWithUnit() throws InterruptedException {
+    new TimedExample().exampleWithUnit();
+    testing.waitAndAssertMetrics(
+        TIMED_INSTRUMENTATION_NAME,
+        metric ->
+            metric
+                .hasName("example.with.unit.duration")
+                .hasUnit("ms")
+                .satisfies(
+                    metricData ->
+                        assertThat(metricData.getHistogramData().getPoints())
+                            .allMatch(p -> p.getMax() < 5000 && p.getMin() > 0)));
+  }
+
+  @Test
+  void testExampleWithAdditionalAttributes1() {
+    new TimedExample().exampleWithAdditionalAttributes1();
+    testing.waitAndAssertMetrics(
+        TIMED_INSTRUMENTATION_NAME,
+        metric ->
+            metric
+                .hasName("example.with.attributes.duration")
+                .satisfies(
+                    metricData ->
+                        assertThat(metricData.getData().getPoints())
+                            .allMatch(
+                                p ->
+                                    "value1"
+                                            .equals(
+                                                p.getAttributes()
+                                                    .get(AttributeKey.stringKey("key1")))
+                                        && "value2"
+                                            .equals(
+                                                p.getAttributes()
+                                                    .get(AttributeKey.stringKey("key2"))))));
+  }
+
+  @Test
+  void testExampleIgnore() throws Exception {
+    new TimedExample().exampleIgnore();
+    Thread.sleep(500);
+    assertThat(testing.metrics()).isEmpty();
+  }
+
+  @Test
+  void testExampleWithException() {
+    try {
+      new TimedExample().exampleWithException();
+    } catch (IllegalStateException e) {
+      // noop
+    }
+    testing.waitAndAssertMetrics(
+        TIMED_INSTRUMENTATION_NAME,
+        metric ->
+            metric
+                .hasName("example.with.exception.duration")
+                .satisfies(
+                    metricData ->
+                        assertThat(metricData.getData().getPoints())
+                            .allMatch(
+                                p ->
+                                    IllegalStateException.class
+                                        .getName()
+                                        .equals(
+                                            p.getAttributes()
+                                                .get(AttributeKey.stringKey("error.type"))))));
+  }
+
+  @Test
+  void testExampleWithReturnValueAttribute() {
+    new TimedExample().exampleWithReturnValueAttribute();
+    testing.waitAndAssertMetrics(
+        TIMED_INSTRUMENTATION_NAME,
+        metric ->
+            metric
+                .hasName("example.with.return.duration")
+                .satisfies(
+                    metricData ->
+                        assertThat(metricData.getData().getPoints())
+                            .allMatch(
+                                p ->
+                                    TimedExample.RETURN_STRING.equals(
+                                        p.getAttributes()
+                                            .get(AttributeKey.stringKey("returnValue"))))));
+  }
+
+  @Test
+  void testCompletableFuture() throws Exception {
+    CompletableFuture<String> future = new CompletableFuture<>();
+    new TimedExample().completableFuture(future);
+
+    Thread.sleep(500); // sleep a bit just to make sure no metric is captured
+    assertThat(testing.metrics()).isEmpty();
+
+    future.complete("Done");
+
+    testing.waitAndAssertMetrics(
+        TIMED_INSTRUMENTATION_NAME,
+        metric ->
+            metric
+                .hasName("example.completable.future.duration")
+                .satisfies(
+                    metricData ->
+                        assertThat(metricData.getData().getPoints())
+                            .allMatch(
+                                p ->
+                                    "Done"
+                                        .equals(
+                                            p.getAttributes()
+                                                .get(AttributeKey.stringKey("returnValue"))))));
+  }
+}
diff --git a/instrumentation/reactor/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/v3_1/ReactorAsyncOperationEndStrategy.java b/instrumentation/reactor/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/v3_1/ReactorAsyncOperationEndStrategy.java
index 49f85ad0aea7..fe1237664f84 100644
--- a/instrumentation/reactor/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/v3_1/ReactorAsyncOperationEndStrategy.java
+++ b/instrumentation/reactor/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/v3_1/ReactorAsyncOperationEndStrategy.java
@@ -10,8 +10,8 @@
 import io.opentelemetry.api.common.AttributeKey;
 import io.opentelemetry.api.trace.Span;
 import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndHandler;
 import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndStrategy;
-import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 import org.reactivestreams.Publisher;
@@ -44,7 +44,7 @@ public boolean supports(Class<?> returnType) {
 
   @Override
   public <REQUEST, RESPONSE> Object end(
-      Instrumenter<REQUEST, RESPONSE> instrumenter,
+      AsyncOperationEndHandler<REQUEST, RESPONSE> handler,
       Context context,
       REQUEST request,
       Object asyncValue,
@@ -54,7 +54,7 @@ public <REQUEST, RESPONSE> Object end(
         new EndOnFirstNotificationConsumer(context) {
           @Override
           protected void end(Object result, Throwable error) {
-            instrumenter.end(context, request, tryToGetResponse(responseType, result), error);
+            handler.handle(context, request, tryToGetResponse(responseType, result), error);
           }
         };
 
diff --git a/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava/v2_0/RxJava2AsyncOperationEndStrategy.java b/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava/v2_0/RxJava2AsyncOperationEndStrategy.java
index a66bda61749a..3c46a1277c42 100644
--- a/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava/v2_0/RxJava2AsyncOperationEndStrategy.java
+++ b/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava/v2_0/RxJava2AsyncOperationEndStrategy.java
@@ -10,8 +10,8 @@
 import io.opentelemetry.api.common.AttributeKey;
 import io.opentelemetry.api.trace.Span;
 import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndHandler;
 import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndStrategy;
-import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
 import io.reactivex.Completable;
 import io.reactivex.Flowable;
 import io.reactivex.Maybe;
@@ -55,7 +55,7 @@ public boolean supports(Class<?> returnType) {
 
   @Override
   public <REQUEST, RESPONSE> Object end(
-      Instrumenter<REQUEST, RESPONSE> instrumenter,
+      AsyncOperationEndHandler<REQUEST, RESPONSE> handler,
       Context context,
       REQUEST request,
       Object asyncValue,
@@ -65,7 +65,7 @@ public <REQUEST, RESPONSE> Object end(
         new EndOnFirstNotificationConsumer<Object>(context) {
           @Override
           protected void end(Object response, Throwable error) {
-            instrumenter.end(context, request, tryToGetResponse(responseType, response), error);
+            handler.handle(context, request, tryToGetResponse(responseType, response), error);
           }
         };
 
diff --git a/instrumentation/rxjava/rxjava-3-common/library/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/RxJava3AsyncOperationEndStrategy.java b/instrumentation/rxjava/rxjava-3-common/library/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/RxJava3AsyncOperationEndStrategy.java
index 2814a15f42dc..c3aedd9f8fe8 100644
--- a/instrumentation/rxjava/rxjava-3-common/library/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/RxJava3AsyncOperationEndStrategy.java
+++ b/instrumentation/rxjava/rxjava-3-common/library/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/RxJava3AsyncOperationEndStrategy.java
@@ -10,8 +10,8 @@
 import io.opentelemetry.api.common.AttributeKey;
 import io.opentelemetry.api.trace.Span;
 import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndHandler;
 import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndStrategy;
-import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
 import io.reactivex.rxjava3.core.Completable;
 import io.reactivex.rxjava3.core.Flowable;
 import io.reactivex.rxjava3.core.Maybe;
@@ -55,7 +55,7 @@ public boolean supports(Class<?> returnType) {
 
   @Override
   public <REQUEST, RESPONSE> Object end(
-      Instrumenter<REQUEST, RESPONSE> instrumenter,
+      AsyncOperationEndHandler<REQUEST, RESPONSE> handler,
       Context context,
       REQUEST request,
       Object asyncValue,
@@ -65,7 +65,7 @@ public <REQUEST, RESPONSE> Object end(
         new EndOnFirstNotificationConsumer<Object>(context) {
           @Override
           protected void end(Object response, Throwable error) {
-            instrumenter.end(context, request, tryToGetResponse(responseType, response), error);
+            handler.handle(context, request, tryToGetResponse(responseType, response), error);
           }
         };
 
diff --git a/javaagent/build.gradle.kts b/javaagent/build.gradle.kts
index 48549c0b2053..00bd0b69cf3f 100644
--- a/javaagent/build.gradle.kts
+++ b/javaagent/build.gradle.kts
@@ -77,7 +77,8 @@ dependencies {
   baseJavaagentLibs(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:javaagent"))
   baseJavaagentLibs(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.4:javaagent"))
   baseJavaagentLibs(project(":instrumentation:opentelemetry-instrumentation-api:javaagent"))
-  baseJavaagentLibs(project(":instrumentation:opentelemetry-instrumentation-annotations-1.16:javaagent"))
+  baseJavaagentLibs(project(":instrumentation:opentelemetry-instrumentation-annotations:opentelemetry-instrumentation-annotations-1.16:javaagent"))
+  baseJavaagentLibs(project(":instrumentation:opentelemetry-instrumentation-annotations:opentelemetry-instrumentation-annotations-incubator:javaagent"))
   baseJavaagentLibs(project(":instrumentation:executors:javaagent"))
   baseJavaagentLibs(project(":instrumentation:internal:internal-application-logger:javaagent"))
   baseJavaagentLibs(project(":instrumentation:internal:internal-class-loader:javaagent"))
diff --git a/opentelemetry-instrumentation-annotations-shaded-for-instrumenting/build.gradle.kts b/opentelemetry-instrumentation-annotations-shaded-for-instrumenting/build.gradle.kts
index 177ebb817e6d..83bc55719112 100644
--- a/opentelemetry-instrumentation-annotations-shaded-for-instrumenting/build.gradle.kts
+++ b/opentelemetry-instrumentation-annotations-shaded-for-instrumenting/build.gradle.kts
@@ -8,6 +8,7 @@ group = "io.opentelemetry.javaagent"
 
 dependencies {
   implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations")
+  implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations-incubator")
 }
 
 // OpenTelemetry Instrumentation Annotations shaded so that it can be used in instrumentation of
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 8de92d20c588..590674c4d1b6 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -93,6 +93,7 @@ include(":bom-alpha")
 include(":instrumentation-api")
 include(":instrumentation-api-incubator")
 include(":instrumentation-annotations")
+include(":instrumentation-annotations-incubator")
 include(":instrumentation-annotations-support")
 include(":instrumentation-annotations-support-testing")
 
@@ -428,7 +429,9 @@ include(":instrumentation:opentelemetry-api:opentelemetry-api-1.40:javaagent")
 include(":instrumentation:opentelemetry-api:opentelemetry-api-1.42:javaagent")
 include(":instrumentation:opentelemetry-extension-annotations-1.0:javaagent")
 include(":instrumentation:opentelemetry-extension-kotlin-1.0:javaagent")
-include(":instrumentation:opentelemetry-instrumentation-annotations-1.16:javaagent")
+include(":instrumentation:opentelemetry-instrumentation-annotations:opentelemetry-instrumentation-annotations-1.16:javaagent")
+include(":instrumentation:opentelemetry-instrumentation-annotations:opentelemetry-instrumentation-annotations-common:javaagent")
+include(":instrumentation:opentelemetry-instrumentation-annotations:opentelemetry-instrumentation-annotations-incubator:javaagent")
 include(":instrumentation:opentelemetry-instrumentation-api:javaagent")
 include(":instrumentation:opentelemetry-instrumentation-api:testing")
 include(":instrumentation:oracle-ucp-11.2:javaagent")