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
This commit is contained in:
Mateusz Rzeszutek 2022-01-03 13:33:39 +01:00 committed by GitHub
parent 606f39c9c7
commit a022f0ce59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1157 additions and 0 deletions

View File

@ -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")
}

View File

@ -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;
}
}
}

View File

@ -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() {}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}

View File

@ -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() {}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}

View File

@ -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() {}
}

View File

@ -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))));
}
}

View File

@ -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);
}
}

View File

@ -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"))));
}
}

View File

@ -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:library")
include(":instrumentation:logback:logback-mdc-1.0:testing") include(":instrumentation:logback:logback-mdc-1.0:testing")
include(":instrumentation:methods:javaagent") include(":instrumentation:methods:javaagent")
include(":instrumentation:micrometer:micrometer-1.5:javaagent")
include(":instrumentation:mongo:mongo-3.1:javaagent") include(":instrumentation:mongo:mongo-3.1:javaagent")
include(":instrumentation:mongo:mongo-3.1:library") include(":instrumentation:mongo:mongo-3.1:library")
include(":instrumentation:mongo:mongo-3.1:testing") include(":instrumentation:mongo:mongo-3.1:testing")