Browse Source

Micrometer bridge instrumentation (#4919)

* Micrometer bridge instrumentation

* gauges with the same name and different attributes

* weak ref gauge

* one more test

* disable by default + muzzle

* code review comments

* log one-time warning

* make AsyncInstrumentRegistry actually thread safe

* code review comments

* one more minor fix
Mateusz Rzeszutek 3 years ago
parent
commit
a022f0ce59
16 changed files with 1157 additions and 0 deletions
  1. 21 0
      instrumentation/micrometer/micrometer-1.5/javaagent/build.gradle.kts
  2. 120 0
      instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/AsyncInstrumentRegistry.java
  3. 41 0
      instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/Bridging.java
  4. 38 0
      instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MetricsInstrumentation.java
  5. 40 0
      instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerInstrumentationModule.java
  6. 21 0
      instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerSingletons.java
  7. 69 0
      instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryCounter.java
  8. 56 0
      instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryGauge.java
  9. 120 0
      instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryMeterRegistry.java
  10. 172 0
      instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryTimer.java
  11. 11 0
      instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/RemovableMeter.java
  12. 23 0
      instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/UnsupportedReadLogger.java
  13. 83 0
      instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/CounterTest.java
  14. 146 0
      instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/GaugeTest.java
  15. 195 0
      instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/TimerTest.java
  16. 1 0
      settings.gradle.kts

+ 21 - 0
instrumentation/micrometer/micrometer-1.5/javaagent/build.gradle.kts

@@ -0,0 +1,21 @@
+plugins {
+  id("otel.javaagent-instrumentation")
+}
+
+muzzle {
+  pass {
+    group.set("io.micrometer")
+    module.set("micrometer-core")
+    versions.set("[1.5.0,)")
+    assertInverse.set(true)
+  }
+}
+
+dependencies {
+  library("io.micrometer:micrometer-core:1.5.0")
+}
+
+// TODO: disabled by default, since not all instruments are implemented
+tasks.withType<Test>().configureEach {
+  jvmArgs("-Dotel.instrumentation.micrometer.enabled=true")
+}

+ 120 - 0
instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/AsyncInstrumentRegistry.java

@@ -0,0 +1,120 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.micrometer.v1_5;
+
+import static io.opentelemetry.javaagent.instrumentation.micrometer.v1_5.Bridging.baseUnit;
+import static io.opentelemetry.javaagent.instrumentation.micrometer.v1_5.Bridging.description;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.Meter;
+import io.opentelemetry.api.metrics.ObservableDoubleMeasurement;
+import io.opentelemetry.instrumentation.api.internal.GuardedBy;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.ToDoubleFunction;
+import javax.annotation.Nullable;
+
+final class AsyncInstrumentRegistry {
+
+  private final Meter meter;
+
+  @GuardedBy("gauges")
+  private final Map<String, GaugeMeasurementsRecorder> gauges = new HashMap<>();
+
+  AsyncInstrumentRegistry(Meter meter) {
+    this.meter = meter;
+  }
+
+  <T> void buildGauge(
+      io.micrometer.core.instrument.Meter.Id meterId,
+      Attributes attributes,
+      @Nullable T obj,
+      ToDoubleFunction<T> objMetric) {
+
+    synchronized (gauges) {
+      GaugeMeasurementsRecorder recorder =
+          gauges.computeIfAbsent(
+              meterId.getName(),
+              n -> {
+                GaugeMeasurementsRecorder recorderCallback = new GaugeMeasurementsRecorder();
+                meter
+                    .gaugeBuilder(meterId.getName())
+                    .setDescription(description(meterId))
+                    .setUnit(baseUnit(meterId))
+                    .buildWithCallback(recorderCallback);
+                return recorderCallback;
+              });
+      recorder.addGaugeMeasurement(attributes, obj, objMetric);
+    }
+  }
+
+  void removeGauge(String name, Attributes attributes) {
+    synchronized (gauges) {
+      GaugeMeasurementsRecorder recorder = gauges.get(name);
+      if (recorder != null) {
+        recorder.removeGaugeMeasurement(attributes);
+        // if this was the last measurement then let's remove the whole recorder
+        if (recorder.isEmpty()) {
+          gauges.remove(name);
+        }
+      }
+    }
+  }
+
+  private final class GaugeMeasurementsRecorder implements Consumer<ObservableDoubleMeasurement> {
+
+    @GuardedBy("gauges")
+    private final Map<Attributes, GaugeInfo> measurements = new HashMap<>();
+
+    @Override
+    public void accept(ObservableDoubleMeasurement measurement) {
+      Map<Attributes, GaugeInfo> measurementsCopy;
+      synchronized (gauges) {
+        measurementsCopy = new HashMap<>(measurements);
+      }
+
+      measurementsCopy.forEach(
+          (attributes, gauge) -> {
+            Object obj = gauge.objWeakRef.get();
+            if (obj != null) {
+              measurement.record(gauge.metricFunction.applyAsDouble(obj), attributes);
+            }
+          });
+    }
+
+    <T> void addGaugeMeasurement(
+        Attributes attributes, @Nullable T obj, ToDoubleFunction<T> objMetric) {
+      synchronized (gauges) {
+        measurements.put(attributes, new GaugeInfo(obj, (ToDoubleFunction<Object>) objMetric));
+      }
+    }
+
+    void removeGaugeMeasurement(Attributes attributes) {
+      synchronized (gauges) {
+        measurements.remove(attributes);
+      }
+    }
+
+    boolean isEmpty() {
+      synchronized (gauges) {
+        return measurements.isEmpty();
+      }
+    }
+  }
+
+  private static final class GaugeInfo {
+
+    private final WeakReference<Object> objWeakRef;
+    private final ToDoubleFunction<Object> metricFunction;
+
+    private GaugeInfo(@Nullable Object obj, ToDoubleFunction<Object> metricFunction) {
+      this.objWeakRef = new WeakReference<>(obj);
+      this.metricFunction = metricFunction;
+    }
+  }
+}

+ 41 - 0
instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/Bridging.java

@@ -0,0 +1,41 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.micrometer.v1_5;
+
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.Tag;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.instrumentation.api.cache.Cache;
+
+final class Bridging {
+
+  private static final Cache<String, AttributeKey<String>> tagsCache = Cache.bounded(1024);
+
+  static Attributes toAttributes(Iterable<Tag> tags) {
+    if (!tags.iterator().hasNext()) {
+      return Attributes.empty();
+    }
+    AttributesBuilder builder = Attributes.builder();
+    for (Tag tag : tags) {
+      builder.put(tagsCache.computeIfAbsent(tag.getKey(), AttributeKey::stringKey), tag.getValue());
+    }
+    return builder.build();
+  }
+
+  static String description(Meter.Id id) {
+    String description = id.getDescription();
+    return description == null ? "" : description;
+  }
+
+  static String baseUnit(Meter.Id id) {
+    String baseUnit = id.getBaseUnit();
+    return baseUnit == null ? "1" : baseUnit;
+  }
+
+  private Bridging() {}
+}

+ 38 - 0
instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MetricsInstrumentation.java

@@ -0,0 +1,38 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.micrometer.v1_5;
+
+import static net.bytebuddy.matcher.ElementMatchers.isTypeInitializer;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+
+import io.micrometer.core.instrument.Metrics;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class MetricsInstrumentation implements TypeInstrumentation {
+
+  @Override
+  public ElementMatcher<TypeDescription> typeMatcher() {
+    return named("io.micrometer.core.instrument.Metrics");
+  }
+
+  @Override
+  public void transform(TypeTransformer transformer) {
+    transformer.applyAdviceToMethod(
+        isTypeInitializer(), this.getClass().getName() + "$StaticInitializerAdvice");
+  }
+
+  @SuppressWarnings("unused")
+  public static class StaticInitializerAdvice {
+    @Advice.OnMethodExit(suppress = Throwable.class)
+    public static void onExit() {
+      Metrics.addRegistry(MicrometerSingletons.meterRegistry());
+    }
+  }
+}

+ 40 - 0
instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerInstrumentationModule.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.micrometer.v1_5;
+
+import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import java.util.Collections;
+import java.util.List;
+import net.bytebuddy.matcher.ElementMatcher;
+
+@AutoService(InstrumentationModule.class)
+public class MicrometerInstrumentationModule extends InstrumentationModule {
+
+  public MicrometerInstrumentationModule() {
+    super("micrometer", "micrometer-1.5");
+  }
+
+  @Override
+  public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
+    // added in 1.5
+    return hasClassesNamed("io.micrometer.core.instrument.config.validate.Validated");
+  }
+
+  @Override
+  protected boolean defaultEnabled() {
+    // TODO: disabled by default, since not all instruments are implemented
+    return false;
+  }
+
+  @Override
+  public List<TypeInstrumentation> typeInstrumentations() {
+    return Collections.singletonList(new MetricsInstrumentation());
+  }
+}

+ 21 - 0
instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerSingletons.java

@@ -0,0 +1,21 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.micrometer.v1_5;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+
+public final class MicrometerSingletons {
+
+  private static final MeterRegistry METER_REGISTRY =
+      OpenTelemetryMeterRegistry.create(GlobalOpenTelemetry.get());
+
+  public static MeterRegistry meterRegistry() {
+    return METER_REGISTRY;
+  }
+
+  private MicrometerSingletons() {}
+}

+ 69 - 0
instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryCounter.java

@@ -0,0 +1,69 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.micrometer.v1_5;
+
+import static io.opentelemetry.javaagent.instrumentation.micrometer.v1_5.Bridging.baseUnit;
+import static io.opentelemetry.javaagent.instrumentation.micrometer.v1_5.Bridging.description;
+import static io.opentelemetry.javaagent.instrumentation.micrometer.v1_5.Bridging.toAttributes;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.Measurement;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleCounter;
+import io.opentelemetry.api.metrics.Meter;
+import java.util.Collections;
+
+final class OpenTelemetryCounter implements Counter, RemovableMeter {
+
+  private final Id id;
+  // TODO: use bound instruments when they're available
+  private final DoubleCounter otelCounter;
+  private final Attributes attributes;
+
+  private volatile boolean removed = false;
+
+  OpenTelemetryCounter(Id id, Meter otelMeter) {
+    this.id = id;
+    this.otelCounter =
+        otelMeter
+            .counterBuilder(id.getName())
+            .setDescription(description(id))
+            .setUnit(baseUnit(id))
+            .ofDoubles()
+            .build();
+    this.attributes = toAttributes(id.getTags());
+  }
+
+  @Override
+  public void increment(double v) {
+    if (removed) {
+      return;
+    }
+    otelCounter.add(v, attributes);
+  }
+
+  @Override
+  public double count() {
+    UnsupportedReadLogger.logWarning();
+    return Double.NaN;
+  }
+
+  @Override
+  public Iterable<Measurement> measure() {
+    UnsupportedReadLogger.logWarning();
+    return Collections.emptyList();
+  }
+
+  @Override
+  public Id getId() {
+    return id;
+  }
+
+  @Override
+  public void onRemove() {
+    removed = true;
+  }
+}

+ 56 - 0
instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryGauge.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.micrometer.v1_5;
+
+import static io.opentelemetry.javaagent.instrumentation.micrometer.v1_5.Bridging.toAttributes;
+
+import io.micrometer.core.instrument.Gauge;
+import io.micrometer.core.instrument.Measurement;
+import io.opentelemetry.api.common.Attributes;
+import java.util.Collections;
+import java.util.function.ToDoubleFunction;
+import javax.annotation.Nullable;
+
+final class OpenTelemetryGauge<T> implements Gauge, RemovableMeter {
+
+  private final Id id;
+  private final Attributes attributes;
+  private final AsyncInstrumentRegistry asyncInstrumentRegistry;
+
+  OpenTelemetryGauge(
+      Id id,
+      @Nullable T obj,
+      ToDoubleFunction<T> objMetric,
+      AsyncInstrumentRegistry asyncInstrumentRegistry) {
+    this.id = id;
+    this.attributes = toAttributes(id.getTags());
+    this.asyncInstrumentRegistry = asyncInstrumentRegistry;
+
+    asyncInstrumentRegistry.buildGauge(id, attributes, obj, objMetric);
+  }
+
+  @Override
+  public double value() {
+    UnsupportedReadLogger.logWarning();
+    return Double.NaN;
+  }
+
+  @Override
+  public Iterable<Measurement> measure() {
+    UnsupportedReadLogger.logWarning();
+    return Collections.emptyList();
+  }
+
+  @Override
+  public Id getId() {
+    return id;
+  }
+
+  @Override
+  public void onRemove() {
+    asyncInstrumentRegistry.removeGauge(id.getName(), attributes);
+  }
+}

+ 120 - 0
instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryMeterRegistry.java

@@ -0,0 +1,120 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.micrometer.v1_5;
+
+import io.micrometer.core.instrument.Clock;
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.DistributionSummary;
+import io.micrometer.core.instrument.FunctionCounter;
+import io.micrometer.core.instrument.FunctionTimer;
+import io.micrometer.core.instrument.Gauge;
+import io.micrometer.core.instrument.LongTaskTimer;
+import io.micrometer.core.instrument.Measurement;
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Timer;
+import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
+import io.micrometer.core.instrument.distribution.HistogramGauges;
+import io.micrometer.core.instrument.distribution.pause.PauseDetector;
+import io.opentelemetry.api.OpenTelemetry;
+import java.util.concurrent.TimeUnit;
+import java.util.function.ToDoubleFunction;
+import java.util.function.ToLongFunction;
+import javax.annotation.Nullable;
+
+public final class OpenTelemetryMeterRegistry extends MeterRegistry {
+
+  private static final String INSTRUMENTATION_NAME = "io.opentelemetry.micrometer-1.5";
+
+  public static MeterRegistry create(OpenTelemetry openTelemetry) {
+    OpenTelemetryMeterRegistry openTelemetryMeterRegistry =
+        new OpenTelemetryMeterRegistry(
+            Clock.SYSTEM, openTelemetry.getMeterProvider().get(INSTRUMENTATION_NAME));
+    openTelemetryMeterRegistry.config().onMeterRemoved(OpenTelemetryMeterRegistry::onMeterRemoved);
+    return openTelemetryMeterRegistry;
+  }
+
+  private final io.opentelemetry.api.metrics.Meter otelMeter;
+  private final AsyncInstrumentRegistry asyncInstrumentRegistry;
+
+  private OpenTelemetryMeterRegistry(Clock clock, io.opentelemetry.api.metrics.Meter otelMeter) {
+    super(clock);
+    this.otelMeter = otelMeter;
+    this.asyncInstrumentRegistry = new AsyncInstrumentRegistry(otelMeter);
+  }
+
+  @Override
+  protected <T> Gauge newGauge(Meter.Id id, @Nullable T t, ToDoubleFunction<T> toDoubleFunction) {
+    return new OpenTelemetryGauge<>(id, t, toDoubleFunction, asyncInstrumentRegistry);
+  }
+
+  @Override
+  protected Counter newCounter(Meter.Id id) {
+    return new OpenTelemetryCounter(id, otelMeter);
+  }
+
+  @Override
+  protected LongTaskTimer newLongTaskTimer(
+      Meter.Id id, DistributionStatisticConfig distributionStatisticConfig) {
+    throw new UnsupportedOperationException("Not implemented yet");
+  }
+
+  @Override
+  protected Timer newTimer(
+      Meter.Id id,
+      DistributionStatisticConfig distributionStatisticConfig,
+      PauseDetector pauseDetector) {
+    OpenTelemetryTimer timer =
+        new OpenTelemetryTimer(id, clock, distributionStatisticConfig, pauseDetector, otelMeter);
+    if (timer.isUsingMicrometerHistograms()) {
+      HistogramGauges.registerWithCommonFormat(timer, this);
+    }
+    return timer;
+  }
+
+  @Override
+  protected DistributionSummary newDistributionSummary(
+      Meter.Id id, DistributionStatisticConfig distributionStatisticConfig, double v) {
+    throw new UnsupportedOperationException("Not implemented yet");
+  }
+
+  @Override
+  protected Meter newMeter(Meter.Id id, Meter.Type type, Iterable<Measurement> iterable) {
+    throw new UnsupportedOperationException("Not implemented yet");
+  }
+
+  @Override
+  protected <T> FunctionTimer newFunctionTimer(
+      Meter.Id id,
+      T t,
+      ToLongFunction<T> toLongFunction,
+      ToDoubleFunction<T> toDoubleFunction,
+      TimeUnit timeUnit) {
+    throw new UnsupportedOperationException("Not implemented yet");
+  }
+
+  @Override
+  protected <T> FunctionCounter newFunctionCounter(
+      Meter.Id id, T t, ToDoubleFunction<T> toDoubleFunction) {
+    throw new UnsupportedOperationException("Not implemented yet");
+  }
+
+  @Override
+  protected TimeUnit getBaseTimeUnit() {
+    return TimeUnit.MILLISECONDS;
+  }
+
+  @Override
+  protected DistributionStatisticConfig defaultHistogramConfig() {
+    return DistributionStatisticConfig.DEFAULT;
+  }
+
+  private static void onMeterRemoved(Meter meter) {
+    if (meter instanceof RemovableMeter) {
+      ((RemovableMeter) meter).onRemove();
+    }
+  }
+}

+ 172 - 0
instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryTimer.java

@@ -0,0 +1,172 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.micrometer.v1_5;
+
+import static io.opentelemetry.javaagent.instrumentation.micrometer.v1_5.Bridging.description;
+import static io.opentelemetry.javaagent.instrumentation.micrometer.v1_5.Bridging.toAttributes;
+
+import io.micrometer.core.instrument.AbstractTimer;
+import io.micrometer.core.instrument.Clock;
+import io.micrometer.core.instrument.Measurement;
+import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
+import io.micrometer.core.instrument.distribution.NoopHistogram;
+import io.micrometer.core.instrument.distribution.TimeWindowMax;
+import io.micrometer.core.instrument.distribution.pause.PauseDetector;
+import io.micrometer.core.instrument.util.TimeUtils;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleHistogram;
+import io.opentelemetry.api.metrics.Meter;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.LongAdder;
+
+final class OpenTelemetryTimer extends AbstractTimer implements RemovableMeter {
+
+  private static final double NANOS_PER_MS = TimeUnit.MILLISECONDS.toNanos(1);
+
+  // TODO: use bound instruments when they're available
+  private final DoubleHistogram otelHistogram;
+  private final Attributes attributes;
+  private final Measurements measurements;
+
+  private volatile boolean removed = false;
+
+  OpenTelemetryTimer(
+      Id id,
+      Clock clock,
+      DistributionStatisticConfig distributionStatisticConfig,
+      PauseDetector pauseDetector,
+      Meter otelMeter) {
+    super(id, clock, distributionStatisticConfig, pauseDetector, TimeUnit.MILLISECONDS, false);
+
+    this.otelHistogram =
+        otelMeter
+            .histogramBuilder(id.getName())
+            .setDescription(description(id))
+            .setUnit("ms")
+            .build();
+    this.attributes = toAttributes(id.getTags());
+
+    if (isUsingMicrometerHistograms()) {
+      measurements = new MicrometerHistogramMeasurements(clock, distributionStatisticConfig);
+    } else {
+      measurements = NoopMeasurements.INSTANCE;
+    }
+  }
+
+  boolean isUsingMicrometerHistograms() {
+    return histogram != NoopHistogram.INSTANCE;
+  }
+
+  @Override
+  protected void recordNonNegative(long amount, TimeUnit unit) {
+    if (amount >= 0 && !removed) {
+      long nanos = unit.toNanos(amount);
+      double time = nanos / NANOS_PER_MS;
+      otelHistogram.record(time, attributes);
+      measurements.record(nanos);
+    }
+  }
+
+  @Override
+  public long count() {
+    return measurements.count();
+  }
+
+  @Override
+  public double totalTime(TimeUnit unit) {
+    return measurements.totalTime(unit);
+  }
+
+  @Override
+  public double max(TimeUnit unit) {
+    return measurements.max(unit);
+  }
+
+  @Override
+  public Iterable<Measurement> measure() {
+    UnsupportedReadLogger.logWarning();
+    return Collections.emptyList();
+  }
+
+  @Override
+  public void onRemove() {
+    removed = true;
+  }
+
+  private interface Measurements {
+    void record(long nanos);
+
+    long count();
+
+    double totalTime(TimeUnit unit);
+
+    double max(TimeUnit unit);
+  }
+
+  // if micrometer histograms are not being used then there's no need to keep any local state
+  // OpenTelemetry metrics bridge does not support reading measurements
+  enum NoopMeasurements implements Measurements {
+    INSTANCE;
+
+    @Override
+    public void record(long nanos) {}
+
+    @Override
+    public long count() {
+      UnsupportedReadLogger.logWarning();
+      return 0;
+    }
+
+    @Override
+    public double totalTime(TimeUnit unit) {
+      UnsupportedReadLogger.logWarning();
+      return Double.NaN;
+    }
+
+    @Override
+    public double max(TimeUnit unit) {
+      UnsupportedReadLogger.logWarning();
+      return Double.NaN;
+    }
+  }
+
+  // calculate count, totalTime and max value for the use of micrometer histograms
+  // kinda similar to how DropwizardTimer does that
+  private static final class MicrometerHistogramMeasurements implements Measurements {
+
+    private final LongAdder count = new LongAdder();
+    private final LongAdder totalTime = new LongAdder();
+    private final TimeWindowMax max;
+
+    MicrometerHistogramMeasurements(
+        Clock clock, DistributionStatisticConfig distributionStatisticConfig) {
+      this.max = new TimeWindowMax(clock, distributionStatisticConfig);
+    }
+
+    @Override
+    public void record(long nanos) {
+      count.increment();
+      totalTime.add(nanos);
+      max.record(nanos, TimeUnit.NANOSECONDS);
+    }
+
+    @Override
+    public long count() {
+      return count.sum();
+    }
+
+    @Override
+    public double totalTime(TimeUnit unit) {
+      return TimeUtils.nanosToUnit(totalTime.sum(), unit);
+    }
+
+    @Override
+    public double max(TimeUnit unit) {
+      return max.poll(unit);
+    }
+  }
+}

+ 11 - 0
instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/RemovableMeter.java

@@ -0,0 +1,11 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.micrometer.v1_5;
+
+interface RemovableMeter {
+
+  void onRemove();
+}

+ 23 - 0
instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/UnsupportedReadLogger.java

@@ -0,0 +1,23 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.micrometer.v1_5;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+final class UnsupportedReadLogger {
+
+  static {
+    Logger logger = LoggerFactory.getLogger(OpenTelemetryMeterRegistry.class);
+    logger.warn("OpenTelemetry metrics bridge does not support reading measurements");
+  }
+
+  static void logWarning() {
+    // do nothing; the warning will be logged exactly once when this class is loaded
+  }
+
+  private UnsupportedReadLogger() {}
+}

+ 83 - 0
instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/CounterTest.java

@@ -0,0 +1,83 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.micrometer.v1_5;
+
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry;
+import static io.opentelemetry.sdk.testing.assertj.metrics.MetricAssertions.assertThat;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.Metrics;
+import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class CounterTest {
+
+  static final String INSTRUMENTATION_NAME = "io.opentelemetry.micrometer-1.5";
+
+  @RegisterExtension
+  static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
+
+  @BeforeEach
+  void cleanupMeters() {
+    Metrics.globalRegistry.forEachMeter(Metrics.globalRegistry::remove);
+  }
+
+  @Test
+  void testCounter() {
+    // given
+    Counter counter =
+        Counter.builder("testCounter")
+            .description("This is a test counter")
+            .tags("tag", "value")
+            .baseUnit("items")
+            .register(Metrics.globalRegistry);
+
+    // when
+    counter.increment();
+    counter.increment(2);
+
+    // then
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        "testCounter",
+        metrics ->
+            metrics.anySatisfy(
+                metric ->
+                    assertThat(metric)
+                        .hasDescription("This is a test counter")
+                        .hasUnit("items")
+                        .hasDoubleSum()
+                        .isMonotonic()
+                        .isCumulative()
+                        .points()
+                        .satisfiesExactly(
+                            point ->
+                                assertThat(point)
+                                    .hasValue(3)
+                                    .attributes()
+                                    .containsOnly(attributeEntry("tag", "value")))));
+    testing.clearData();
+
+    // when
+    Metrics.globalRegistry.remove(counter);
+    counter.increment();
+
+    // then
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        "testCounter",
+        metrics ->
+            metrics.allSatisfy(
+                metric ->
+                    assertThat(metric)
+                        .hasDoubleSum()
+                        .points()
+                        .noneSatisfy(point -> assertThat(point).hasValue(4))));
+  }
+}

+ 146 - 0
instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/GaugeTest.java

@@ -0,0 +1,146 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.micrometer.v1_5;
+
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry;
+import static io.opentelemetry.sdk.testing.assertj.metrics.MetricAssertions.assertThat;
+
+import io.micrometer.core.instrument.Gauge;
+import io.micrometer.core.instrument.Metrics;
+import io.opentelemetry.instrumentation.test.utils.GcUtils;
+import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import java.lang.ref.WeakReference;
+import java.util.concurrent.atomic.AtomicLong;
+import org.assertj.core.api.AbstractIterableAssert;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class GaugeTest {
+
+  static final String INSTRUMENTATION_NAME = "io.opentelemetry.micrometer-1.5";
+
+  @RegisterExtension
+  static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
+
+  @BeforeEach
+  void cleanupMeters() {
+    Metrics.globalRegistry.forEachMeter(Metrics.globalRegistry::remove);
+  }
+
+  @Test
+  void testGauge() throws Exception {
+    // when
+    Gauge gauge =
+        Gauge.builder("testGauge", () -> 42)
+            .description("This is a test gauge")
+            .tags("tag", "value")
+            .baseUnit("items")
+            .register(Metrics.globalRegistry);
+
+    // then
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        "testGauge",
+        metrics ->
+            metrics.anySatisfy(
+                metric ->
+                    assertThat(metric)
+                        .hasDescription("This is a test gauge")
+                        .hasUnit("items")
+                        .hasDoubleGauge()
+                        .points()
+                        .satisfiesExactly(
+                            point ->
+                                assertThat(point)
+                                    .hasValue(42)
+                                    .attributes()
+                                    .containsOnly(attributeEntry("tag", "value")))));
+
+    // when
+    Metrics.globalRegistry.remove(gauge);
+    Thread.sleep(10); // give time for any inflight metric export to be received
+    testing.clearData();
+
+    // then
+    Thread.sleep(100); // interval of the test metrics exporter
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME, "testGauge", AbstractIterableAssert::isEmpty);
+  }
+
+  @Test
+  void gaugesWithSameNameAndDifferentTags() {
+    // when
+    Gauge.builder("testGaugeWithTags", () -> 12)
+        .description("First description wins")
+        .baseUnit("items")
+        .tags("tag", "1")
+        .register(Metrics.globalRegistry);
+    Gauge.builder("testGaugeWithTags", () -> 42)
+        .description("ignored")
+        .baseUnit("ignored")
+        .tags("tag", "2")
+        .register(Metrics.globalRegistry);
+
+    // then
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        "testGaugeWithTags",
+        metrics ->
+            metrics.anySatisfy(
+                metric ->
+                    assertThat(metric)
+                        .hasDescription("First description wins")
+                        .hasUnit("items")
+                        .hasDoubleGauge()
+                        .points()
+                        .anySatisfy(
+                            point ->
+                                assertThat(point)
+                                    .hasValue(12)
+                                    .attributes()
+                                    .containsOnly(attributeEntry("tag", "1")))
+                        .anySatisfy(
+                            point ->
+                                assertThat(point)
+                                    .hasValue(42)
+                                    .attributes()
+                                    .containsOnly(attributeEntry("tag", "2")))));
+  }
+
+  @Test
+  void testWeakRefGauge() throws InterruptedException {
+    // when
+    AtomicLong num = new AtomicLong(42);
+    Gauge.builder("testWeakRefGauge", num, AtomicLong::get)
+        .strongReference(false)
+        .register(Metrics.globalRegistry);
+
+    // then
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        "testWeakRefGauge",
+        metrics ->
+            metrics.anySatisfy(
+                metric ->
+                    assertThat(metric)
+                        .hasDoubleGauge()
+                        .points()
+                        .satisfiesExactly(point -> assertThat(point).hasValue(42))));
+    testing.clearData();
+
+    // when
+    WeakReference<AtomicLong> numWeakRef = new WeakReference<>(num);
+    num = null;
+    GcUtils.awaitGc(numWeakRef);
+
+    // then
+    Thread.sleep(100); // interval of the test metrics exporter
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME, "testWeakRefGauge", AbstractIterableAssert::isEmpty);
+  }
+}

+ 195 - 0
instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/TimerTest.java

@@ -0,0 +1,195 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.micrometer.v1_5;
+
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry;
+import static io.opentelemetry.sdk.testing.assertj.metrics.MetricAssertions.assertThat;
+
+import io.micrometer.core.instrument.Metrics;
+import io.micrometer.core.instrument.Timer;
+import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+@SuppressWarnings("PreferJavaTimeOverload")
+class TimerTest {
+
+  static final String INSTRUMENTATION_NAME = "io.opentelemetry.micrometer-1.5";
+
+  @RegisterExtension
+  static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
+
+  @BeforeEach
+  void cleanupMeters() {
+    Metrics.globalRegistry.forEachMeter(Metrics.globalRegistry::remove);
+  }
+
+  @Test
+  void testTimer() {
+    // given
+    Timer timer =
+        Timer.builder("testTimer")
+            .description("This is a test timer")
+            .tags("tag", "value")
+            .register(Metrics.globalRegistry);
+
+    // when
+    timer.record(42, TimeUnit.SECONDS);
+
+    // then
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        "testTimer",
+        metrics ->
+            metrics.anySatisfy(
+                metric ->
+                    assertThat(metric)
+                        .hasDescription("This is a test timer")
+                        .hasUnit("ms")
+                        .hasDoubleHistogram()
+                        .points()
+                        .satisfiesExactly(
+                            point ->
+                                assertThat(point)
+                                    .hasSum(42_000)
+                                    .hasCount(1)
+                                    .attributes()
+                                    .containsOnly(attributeEntry("tag", "value")))));
+    testing.clearData();
+
+    // when
+    Metrics.globalRegistry.remove(timer);
+    timer.record(12, TimeUnit.SECONDS);
+
+    // then
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        "testTimer",
+        metrics ->
+            metrics.allSatisfy(
+                metric ->
+                    assertThat(metric)
+                        .hasDoubleHistogram()
+                        .points()
+                        .noneSatisfy(point -> assertThat(point).hasSum(54_000).hasCount(2))));
+  }
+
+  @Test
+  void testNanoPrecision() {
+    // given
+    Timer timer = Timer.builder("testNanoTimer").register(Metrics.globalRegistry);
+
+    // when
+    timer.record(1_234_000, TimeUnit.NANOSECONDS);
+
+    // then
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        "testNanoTimer",
+        metrics ->
+            metrics.anySatisfy(
+                metric ->
+                    assertThat(metric)
+                        .hasUnit("ms")
+                        .hasDoubleHistogram()
+                        .points()
+                        .satisfiesExactly(
+                            point -> assertThat(point).hasSum(1.234).hasCount(1).attributes())));
+  }
+
+  @Test
+  void testMicrometerHistogram() {
+    // given
+    Timer timer =
+        Timer.builder("testHistogram")
+            .description("This is a test timer")
+            .tags("tag", "value")
+            .serviceLevelObjectives(
+                Duration.ofSeconds(1),
+                Duration.ofSeconds(10),
+                Duration.ofSeconds(100),
+                Duration.ofSeconds(1000))
+            .distributionStatisticBufferLength(10)
+            .register(Metrics.globalRegistry);
+
+    // when
+    timer.record(500, TimeUnit.MILLISECONDS);
+    timer.record(5, TimeUnit.SECONDS);
+    timer.record(50, TimeUnit.SECONDS);
+    timer.record(500, TimeUnit.SECONDS);
+
+    // then
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        "testHistogram.histogram",
+        metrics ->
+            metrics.anySatisfy(
+                metric ->
+                    assertThat(metric)
+                        .hasDoubleGauge()
+                        .points()
+                        .anySatisfy(
+                            point ->
+                                assertThat(point)
+                                    .hasValue(1)
+                                    .attributes()
+                                    .containsEntry("le", "1000"))
+                        .anySatisfy(
+                            point ->
+                                assertThat(point)
+                                    .hasValue(2)
+                                    .attributes()
+                                    .containsEntry("le", "10000"))
+                        .anySatisfy(
+                            point ->
+                                assertThat(point)
+                                    .hasValue(3)
+                                    .attributes()
+                                    .containsEntry("le", "100000"))
+                        .anySatisfy(
+                            point ->
+                                assertThat(point)
+                                    .hasValue(4)
+                                    .attributes()
+                                    .containsEntry("le", "1000000"))));
+  }
+
+  @Test
+  void testMicrometerPercentiles() {
+    // given
+    Timer timer =
+        Timer.builder("testPercentiles")
+            .description("This is a test timer")
+            .tags("tag", "value")
+            .publishPercentiles(0.5, 0.95, 0.99)
+            .register(Metrics.globalRegistry);
+
+    // when
+    timer.record(50, TimeUnit.MILLISECONDS);
+    timer.record(100, TimeUnit.MILLISECONDS);
+
+    // then
+    testing.waitAndAssertMetrics(
+        INSTRUMENTATION_NAME,
+        "testPercentiles.percentile",
+        metrics ->
+            metrics.anySatisfy(
+                metric ->
+                    assertThat(metric)
+                        .hasDoubleGauge()
+                        .points()
+                        .anySatisfy(
+                            point -> assertThat(point).attributes().containsEntry("phi", "0.5"))
+                        .anySatisfy(
+                            point -> assertThat(point).attributes().containsEntry("phi", "0.95"))
+                        .anySatisfy(
+                            point -> assertThat(point).attributes().containsEntry("phi", "0.99"))));
+  }
+}

+ 1 - 0
settings.gradle.kts

@@ -280,6 +280,7 @@ include(":instrumentation:logback:logback-mdc-1.0:javaagent")
 include(":instrumentation:logback:logback-mdc-1.0:library")
 include(":instrumentation:logback:logback-mdc-1.0:testing")
 include(":instrumentation:methods:javaagent")
+include(":instrumentation:micrometer:micrometer-1.5:javaagent")
 include(":instrumentation:mongo:mongo-3.1:javaagent")
 include(":instrumentation:mongo:mongo-3.1:library")
 include(":instrumentation:mongo:mongo-3.1:testing")