From a6b8a2564516c70801382121882baf7a33fc5464 Mon Sep 17 00:00:00 2001 From: Mateusz Rzeszutek Date: Mon, 3 Jan 2022 13:33:39 +0100 Subject: [PATCH] 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 --- .../micrometer-1.5/javaagent/build.gradle.kts | 21 ++ .../v1_5/AsyncInstrumentRegistry.java | 120 +++++++++++ .../micrometer/v1_5/Bridging.java | 41 ++++ .../v1_5/MetricsInstrumentation.java | 38 ++++ .../v1_5/MicrometerInstrumentationModule.java | 40 ++++ .../micrometer/v1_5/MicrometerSingletons.java | 21 ++ .../micrometer/v1_5/OpenTelemetryCounter.java | 69 +++++++ .../micrometer/v1_5/OpenTelemetryGauge.java | 56 +++++ .../v1_5/OpenTelemetryMeterRegistry.java | 120 +++++++++++ .../micrometer/v1_5/OpenTelemetryTimer.java | 172 +++++++++++++++ .../micrometer/v1_5/RemovableMeter.java | 11 + .../v1_5/UnsupportedReadLogger.java | 23 +++ .../micrometer/v1_5/CounterTest.java | 83 ++++++++ .../micrometer/v1_5/GaugeTest.java | 146 +++++++++++++ .../micrometer/v1_5/TimerTest.java | 195 ++++++++++++++++++ settings.gradle.kts | 1 + 16 files changed, 1157 insertions(+) create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/build.gradle.kts create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/AsyncInstrumentRegistry.java create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/Bridging.java create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MetricsInstrumentation.java create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerInstrumentationModule.java create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerSingletons.java create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryCounter.java create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryGauge.java create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryMeterRegistry.java create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryTimer.java create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/RemovableMeter.java create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/UnsupportedReadLogger.java create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/CounterTest.java create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/GaugeTest.java create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/TimerTest.java diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/build.gradle.kts b/instrumentation/micrometer/micrometer-1.5/javaagent/build.gradle.kts new file mode 100644 index 000000000000..f5a1ce9f7b75 --- /dev/null +++ b/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().configureEach { + jvmArgs("-Dotel.instrumentation.micrometer.enabled=true") +} diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/AsyncInstrumentRegistry.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/AsyncInstrumentRegistry.java new file mode 100644 index 000000000000..75d871a183b3 --- /dev/null +++ b/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 gauges = new HashMap<>(); + + AsyncInstrumentRegistry(Meter meter) { + this.meter = meter; + } + + void buildGauge( + io.micrometer.core.instrument.Meter.Id meterId, + Attributes attributes, + @Nullable T obj, + ToDoubleFunction 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 { + + @GuardedBy("gauges") + private final Map measurements = new HashMap<>(); + + @Override + public void accept(ObservableDoubleMeasurement measurement) { + Map 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); + } + }); + } + + void addGaugeMeasurement( + Attributes attributes, @Nullable T obj, ToDoubleFunction objMetric) { + synchronized (gauges) { + measurements.put(attributes, new GaugeInfo(obj, (ToDoubleFunction) 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 objWeakRef; + private final ToDoubleFunction metricFunction; + + private GaugeInfo(@Nullable Object obj, ToDoubleFunction metricFunction) { + this.objWeakRef = new WeakReference<>(obj); + this.metricFunction = metricFunction; + } + } +} diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/Bridging.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/Bridging.java new file mode 100644 index 000000000000..7a3be0b7ef56 --- /dev/null +++ b/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> tagsCache = Cache.bounded(1024); + + static Attributes toAttributes(Iterable 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() {} +} diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MetricsInstrumentation.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MetricsInstrumentation.java new file mode 100644 index 000000000000..6460abf0721a --- /dev/null +++ b/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 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()); + } + } +} diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerInstrumentationModule.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerInstrumentationModule.java new file mode 100644 index 000000000000..49dedf81c600 --- /dev/null +++ b/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 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 typeInstrumentations() { + return Collections.singletonList(new MetricsInstrumentation()); + } +} diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerSingletons.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerSingletons.java new file mode 100644 index 000000000000..45bdec402858 --- /dev/null +++ b/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() {} +} diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryCounter.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryCounter.java new file mode 100644 index 000000000000..5b86ba815711 --- /dev/null +++ b/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 measure() { + UnsupportedReadLogger.logWarning(); + return Collections.emptyList(); + } + + @Override + public Id getId() { + return id; + } + + @Override + public void onRemove() { + removed = true; + } +} diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryGauge.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryGauge.java new file mode 100644 index 000000000000..0e227e0af264 --- /dev/null +++ b/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 implements Gauge, RemovableMeter { + + private final Id id; + private final Attributes attributes; + private final AsyncInstrumentRegistry asyncInstrumentRegistry; + + OpenTelemetryGauge( + Id id, + @Nullable T obj, + ToDoubleFunction 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 measure() { + UnsupportedReadLogger.logWarning(); + return Collections.emptyList(); + } + + @Override + public Id getId() { + return id; + } + + @Override + public void onRemove() { + asyncInstrumentRegistry.removeGauge(id.getName(), attributes); + } +} diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryMeterRegistry.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryMeterRegistry.java new file mode 100644 index 000000000000..c92deb4552d7 --- /dev/null +++ b/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 Gauge newGauge(Meter.Id id, @Nullable T t, ToDoubleFunction 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 iterable) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + protected FunctionTimer newFunctionTimer( + Meter.Id id, + T t, + ToLongFunction toLongFunction, + ToDoubleFunction toDoubleFunction, + TimeUnit timeUnit) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + protected FunctionCounter newFunctionCounter( + Meter.Id id, T t, ToDoubleFunction 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(); + } + } +} diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryTimer.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/OpenTelemetryTimer.java new file mode 100644 index 000000000000..a0b9044d9fc2 --- /dev/null +++ b/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 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); + } + } +} diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/RemovableMeter.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/RemovableMeter.java new file mode 100644 index 000000000000..c60703b8c7a6 --- /dev/null +++ b/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(); +} diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/UnsupportedReadLogger.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/UnsupportedReadLogger.java new file mode 100644 index 000000000000..de9ae1d5178f --- /dev/null +++ b/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() {} +} diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/CounterTest.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/CounterTest.java new file mode 100644 index 000000000000..36f12655b4b2 --- /dev/null +++ b/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)))); + } +} diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/GaugeTest.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/GaugeTest.java new file mode 100644 index 000000000000..221f2bdd0787 --- /dev/null +++ b/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 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); + } +} diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/TimerTest.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/TimerTest.java new file mode 100644 index 000000000000..545e807ca193 --- /dev/null +++ b/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")))); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index b442c2c3e2a8..ec0c9912b324 100644 --- a/settings.gradle.kts +++ b/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")