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:
parent
606f39c9c7
commit
a022f0ce59
|
@ -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")
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -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))));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"))));
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue