Change the way Micrometer LongTaskTimer is bridged (#5338)

This commit is contained in:
Mateusz Rzeszutek 2022-02-18 01:46:23 +01:00 committed by GitHub
parent cc60ffb6e0
commit 6774ce5791
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 196 additions and 228 deletions

View File

@ -64,6 +64,8 @@ public final class AsyncInstrumentRegistry {
private final Map<String, LongMeasurementsRecorder> longCounters = new ConcurrentHashMap<>();
private final Map<String, DoubleMeasurementsRecorder> upDownDoubleCounters =
new ConcurrentHashMap<>();
private final Map<String, LongMeasurementsRecorder> upDownLongCounters =
new ConcurrentHashMap<>();
AsyncInstrumentRegistry(Meter meter) {
this.meter = new WeakReference<>(meter);
@ -150,7 +152,7 @@ public final class AsyncInstrumentRegistry {
String description,
String baseUnit,
Attributes attributes,
T obj,
@Nullable T obj,
ToDoubleFunction<T> objMetric) {
DoubleMeasurementsRecorder recorder =
@ -171,6 +173,31 @@ public final class AsyncInstrumentRegistry {
return new AsyncMeasurementHandle(recorder, attributes);
}
public <T> AsyncMeasurementHandle buildUpDownLongCounter(
String name,
String description,
String baseUnit,
Attributes attributes,
@Nullable T obj,
ToLongFunction<T> objMetric) {
LongMeasurementsRecorder recorder =
upDownLongCounters.computeIfAbsent(
name,
n -> {
LongMeasurementsRecorder recorderCallback = new LongMeasurementsRecorder();
otelMeter()
.upDownCounterBuilder(name)
.setDescription(description)
.setUnit(baseUnit)
.buildWithCallback(recorderCallback);
return recorderCallback;
});
recorder.addMeasurement(attributes, new LongMeasurementSource<>(obj, objMetric));
return new AsyncMeasurementHandle(recorder, attributes);
}
private Meter otelMeter() {
Meter otelMeter = meter.get();
if (otelMeter == null) {

View File

@ -33,11 +33,7 @@ final class Bridging {
}
static String name(Meter.Id id, NamingConvention namingConvention) {
return name(id.getName(), id, namingConvention);
}
private static String name(String name, Meter.Id id, NamingConvention namingConvention) {
return namingConvention.name(name, id.getType(), id.getBaseUnit());
return namingConvention.name(id.getName(), id.getType(), id.getBaseUnit());
}
static String description(Meter.Id id) {
@ -56,7 +52,7 @@ final class Bridging {
// use "total_time" instead of "total" to avoid clashing with Statistic.TOTAL
String statisticStr =
statistic == Statistic.TOTAL_TIME ? "total_time" : statistic.getTagValueRepresentation();
return name(prefix + statisticStr, id, namingConvention);
return namingConvention.name(prefix + statisticStr, id.getType(), id.getBaseUnit());
}
private Bridging() {}

View File

@ -8,14 +8,12 @@ package io.opentelemetry.instrumentation.micrometer.v1_5;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.baseUnit;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.description;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.name;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.statisticInstrumentName;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.tagsAsAttributes;
import io.micrometer.core.instrument.AbstractDistributionSummary;
import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.Measurement;
import io.micrometer.core.instrument.Statistic;
import io.micrometer.core.instrument.config.NamingConvention;
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
import io.micrometer.core.instrument.distribution.NoopHistogram;
@ -59,15 +57,17 @@ final class OpenTelemetryDistributionSummary extends AbstractDistributionSummary
max = new TimeWindowMax(clock, distributionStatisticConfig);
this.attributes = tagsAsAttributes(id, namingConvention);
String conventionName = name(id, namingConvention);
this.otelHistogram =
otelMeter
.histogramBuilder(name(id, namingConvention))
.histogramBuilder(conventionName)
.setDescription(description(id))
.setUnit(baseUnit(id))
.build();
this.maxHandle =
asyncInstrumentRegistry.buildGauge(
statisticInstrumentName(id, Statistic.MAX, namingConvention),
conventionName + ".max",
description(id),
baseUnit(id),
attributes,

View File

@ -6,13 +6,12 @@
package io.opentelemetry.instrumentation.micrometer.v1_5;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.description;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.statisticInstrumentName;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.name;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.tagsAsAttributes;
import static io.opentelemetry.instrumentation.micrometer.v1_5.TimeUnitHelper.getUnitString;
import io.micrometer.core.instrument.FunctionTimer;
import io.micrometer.core.instrument.Measurement;
import io.micrometer.core.instrument.Statistic;
import io.micrometer.core.instrument.config.NamingConvention;
import io.micrometer.core.instrument.util.MeterEquivalence;
import io.micrometer.core.instrument.util.TimeUtils;
@ -46,8 +45,8 @@ final class OpenTelemetryFunctionTimer<T> implements FunctionTimer, RemovableMet
this.id = id;
this.baseTimeUnit = baseTimeUnit;
String countMeterName = statisticInstrumentName(id, Statistic.COUNT, namingConvention);
String totalTimeMeterName = statisticInstrumentName(id, Statistic.TOTAL_TIME, namingConvention);
String countMeterName = name(id, namingConvention) + ".count";
String totalTimeMeterName = name(id, namingConvention) + ".sum";
Attributes attributes = tagsAsAttributes(id, namingConvention);
countMeasurementHandle =

View File

@ -7,34 +7,25 @@ package io.opentelemetry.instrumentation.micrometer.v1_5;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.description;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.name;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.statisticInstrumentName;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.tagsAsAttributes;
import static io.opentelemetry.instrumentation.micrometer.v1_5.TimeUnitHelper.getUnitString;
import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.Measurement;
import io.micrometer.core.instrument.Statistic;
import io.micrometer.core.instrument.config.NamingConvention;
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
import io.micrometer.core.instrument.internal.DefaultLongTaskTimer;
import io.micrometer.core.instrument.util.TimeUtils;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.LongUpDownCounter;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.instrumentation.api.internal.AsyncInstrumentRegistry;
import io.opentelemetry.instrumentation.api.internal.AsyncInstrumentRegistry.AsyncMeasurementHandle;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
final class OpenTelemetryLongTaskTimer extends DefaultLongTaskTimer implements RemovableMeter {
private final TimeUnit baseTimeUnit;
private final DistributionStatisticConfig distributionStatisticConfig;
// TODO: use bound instruments when they're available
private final DoubleHistogram otelHistogram;
private final LongUpDownCounter otelActiveTasksCounter;
private final Attributes attributes;
private volatile boolean removed = false;
private final AsyncMeasurementHandle activeTasksHandle;
private final AsyncMeasurementHandle durationHandle;
OpenTelemetryLongTaskTimer(
Id id,
@ -42,37 +33,29 @@ final class OpenTelemetryLongTaskTimer extends DefaultLongTaskTimer implements R
Clock clock,
TimeUnit baseTimeUnit,
DistributionStatisticConfig distributionStatisticConfig,
Meter otelMeter) {
AsyncInstrumentRegistry asyncInstrumentRegistry) {
super(id, clock, baseTimeUnit, distributionStatisticConfig, false);
this.baseTimeUnit = baseTimeUnit;
this.distributionStatisticConfig = distributionStatisticConfig;
this.otelHistogram =
otelMeter
.histogramBuilder(name(id, namingConvention))
.setDescription(description(id))
.setUnit(getUnitString(baseTimeUnit))
.build();
this.otelActiveTasksCounter =
otelMeter
.upDownCounterBuilder(
statisticInstrumentName(id, Statistic.ACTIVE_TASKS, namingConvention))
.setDescription(description(id))
.setUnit("tasks")
.build();
this.attributes = tagsAsAttributes(id, namingConvention);
}
@Override
public Sample start() {
Sample original = super.start();
if (removed) {
return original;
}
otelActiveTasksCounter.add(1, attributes);
return new OpenTelemetrySample(original);
String conventionName = name(id, namingConvention);
Attributes attributes = tagsAsAttributes(id, namingConvention);
this.activeTasksHandle =
asyncInstrumentRegistry.buildUpDownLongCounter(
conventionName + ".active",
description(id),
"tasks",
attributes,
this,
DefaultLongTaskTimer::activeTasks);
this.durationHandle =
asyncInstrumentRegistry.buildUpDownDoubleCounter(
conventionName + ".duration",
description(id),
getUnitString(baseTimeUnit),
attributes,
this,
t -> t.duration(baseTimeUnit));
}
@Override
@ -83,41 +66,12 @@ final class OpenTelemetryLongTaskTimer extends DefaultLongTaskTimer implements R
@Override
public void onRemove() {
removed = true;
activeTasksHandle.remove();
durationHandle.remove();
}
boolean isUsingMicrometerHistograms() {
return distributionStatisticConfig.isPublishingPercentiles()
|| distributionStatisticConfig.isPublishingHistogram();
}
private final class OpenTelemetrySample extends Sample {
private final Sample original;
private volatile boolean stopped = false;
private OpenTelemetrySample(Sample original) {
this.original = original;
}
@Override
public long stop() {
if (stopped) {
return -1;
}
stopped = true;
long durationNanos = original.stop();
if (!removed) {
otelActiveTasksCounter.add(-1, attributes);
double time = TimeUtils.nanosToUnit(durationNanos, baseTimeUnit);
otelHistogram.record(time, attributes);
}
return durationNanos;
}
@Override
public double duration(TimeUnit unit) {
return stopped ? -1 : original.duration(unit);
}
}
}

View File

@ -87,7 +87,7 @@ public final class OpenTelemetryMeterRegistry extends MeterRegistry {
clock,
getBaseTimeUnit(),
distributionStatisticConfig,
otelMeter);
asyncInstrumentRegistry);
if (timer.isUsingMicrometerHistograms()) {
HistogramGauges.registerWithCommonFormat(timer, this);
}

View File

@ -7,14 +7,12 @@ package io.opentelemetry.instrumentation.micrometer.v1_5;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.description;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.name;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.statisticInstrumentName;
import static io.opentelemetry.instrumentation.micrometer.v1_5.Bridging.tagsAsAttributes;
import static io.opentelemetry.instrumentation.micrometer.v1_5.TimeUnitHelper.getUnitString;
import io.micrometer.core.instrument.AbstractTimer;
import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.Measurement;
import io.micrometer.core.instrument.Statistic;
import io.micrometer.core.instrument.config.NamingConvention;
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
import io.micrometer.core.instrument.distribution.NoopHistogram;
@ -62,15 +60,17 @@ final class OpenTelemetryTimer extends AbstractTimer implements RemovableMeter {
this.baseTimeUnit = baseTimeUnit;
this.attributes = tagsAsAttributes(id, namingConvention);
String conventionName = name(id, namingConvention);
this.otelHistogram =
otelMeter
.histogramBuilder(name(id, namingConvention))
.histogramBuilder(conventionName)
.setDescription(description(id))
.setUnit(getUnitString(baseTimeUnit))
.build();
this.maxHandle =
asyncInstrumentRegistry.buildGauge(
statisticInstrumentName(id, Statistic.MAX, namingConvention),
conventionName + ".max",
description(id),
getUnitString(baseTimeUnit),
attributes,

View File

@ -10,6 +10,7 @@ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attri
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.LongTaskTimer;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Metrics;
@ -178,7 +179,7 @@ class NamingConventionTest {
.containsOnly(attributeEntry("test.tag", "test.value")))));
testing.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"test.renamedFunctionTimer.total_time",
"test.renamedFunctionTimer.sum",
metrics ->
metrics.anySatisfy(
metric ->
@ -214,6 +215,43 @@ class NamingConventionTest {
.containsOnly(attributeEntry("test.tag", "test.value")))));
}
@Test
void renameLongTaskTimer() {
// when
LongTaskTimer timer = Metrics.more().longTaskTimer("renamedLongTaskTimer", "tag", "value");
timer.start().stop();
// then
testing.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"test.renamedLongTaskTimer.active",
metrics ->
metrics.anySatisfy(
metric ->
assertThat(metric)
.hasLongSum()
.points()
.satisfiesExactly(
point ->
assertThat(point)
.attributes()
.containsOnly(attributeEntry("test.tag", "test.value")))));
testing.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"test.renamedLongTaskTimer.duration",
metrics ->
metrics.anySatisfy(
metric ->
assertThat(metric)
.hasDoubleSum()
.points()
.satisfiesExactly(
point ->
assertThat(point)
.attributes()
.containsOnly(attributeEntry("test.tag", "test.value")))));
}
@Test
void renameTimer() {
// when

View File

@ -31,7 +31,7 @@ public abstract class AbstractFunctionTimerSecondsTest {
}
@Test
void testFunctionCounterWithBaseUnitSeconds() throws InterruptedException {
void testFunctionTimerWithBaseUnitSeconds() throws InterruptedException {
// given
FunctionTimer functionTimer =
FunctionTimer.builder(
@ -70,7 +70,7 @@ public abstract class AbstractFunctionTimerSecondsTest {
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testFunctionTimerSeconds.total_time",
"testFunctionTimerSeconds.sum",
metrics ->
metrics.anySatisfy(
metric ->
@ -100,8 +100,6 @@ public abstract class AbstractFunctionTimerSecondsTest {
AbstractIterableAssert::isEmpty);
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testFunctionTimerSeconds.total_time",
AbstractIterableAssert::isEmpty);
INSTRUMENTATION_NAME, "testFunctionTimerSeconds.sum", AbstractIterableAssert::isEmpty);
}
}

View File

@ -72,7 +72,7 @@ public abstract class AbstractFunctionTimerTest {
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testFunctionTimer.total_time",
"testFunctionTimer.sum",
metrics ->
metrics.anySatisfy(
metric ->
@ -100,7 +100,7 @@ public abstract class AbstractFunctionTimerTest {
INSTRUMENTATION_NAME, "testFunctionTimer.count", AbstractIterableAssert::isEmpty);
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME, "testFunctionTimer.total_time", AbstractIterableAssert::isEmpty);
INSTRUMENTATION_NAME, "testFunctionTimer.sum", AbstractIterableAssert::isEmpty);
}
@Test
@ -121,7 +121,7 @@ public abstract class AbstractFunctionTimerTest {
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testNanoFunctionTimer.total_time",
"testNanoFunctionTimer.sum",
metrics ->
metrics.anySatisfy(
metric ->
@ -162,7 +162,7 @@ public abstract class AbstractFunctionTimerTest {
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testFunctionTimerWithTags.total_time",
"testFunctionTimerWithTags.sum",
metrics ->
metrics.anySatisfy(
metric ->

View File

@ -12,6 +12,7 @@ import io.micrometer.core.instrument.LongTaskTimer;
import io.micrometer.core.instrument.Metrics;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import java.util.concurrent.TimeUnit;
import org.assertj.core.api.AbstractIterableAssert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -58,31 +59,33 @@ public abstract class AbstractLongTaskTimerSecondsTest {
.hasValue(1)
.attributes()
.containsOnly(attributeEntry("tag", "value")))));
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testLongTaskTimerSeconds.duration",
metrics ->
metrics.anySatisfy(
metric ->
assertThat(metric)
.hasDescription("This is a test long task timer")
.hasUnit("s")
.hasDoubleSum()
.isNotMonotonic()
.points()
.satisfiesExactly(
point -> {
assertThat(point)
.attributes()
.containsOnly(attributeEntry("tag", "value"));
// any value >0 - duration of currently running tasks
assertThat(point.getValue()).isPositive();
})));
// when
TimeUnit.MILLISECONDS.sleep(100);
sample.stop();
// then
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testLongTaskTimerSeconds",
metrics ->
metrics.anySatisfy(
metric ->
assertThat(metric)
.hasDescription("This is a test long task timer")
.hasUnit("s")
.hasDoubleHistogram()
.points()
.satisfiesExactly(
point ->
assertThat(point)
.hasSumGreaterThan(0.1)
.hasCount(1)
.attributes()
.containsOnly(attributeEntry("tag", "value")))));
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
@ -99,46 +102,38 @@ public abstract class AbstractLongTaskTimerSecondsTest {
.hasValue(0)
.attributes()
.containsOnly(attributeEntry("tag", "value")))));
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testLongTaskTimerSeconds.duration",
metrics ->
metrics.anySatisfy(
metric ->
assertThat(metric)
.hasDoubleSum()
.points()
.satisfiesExactly(
point ->
assertThat(point)
.hasValue(0)
.attributes()
.containsOnly(attributeEntry("tag", "value")))));
testing().clearData();
// when timer is removed from the registry
Metrics.globalRegistry.remove(timer);
sample = timer.start();
timer.start();
// then no tasks are active after starting a new sample
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testLongTaskTimerSeconds.active",
metrics ->
metrics.anySatisfy(
metric ->
assertThat(metric)
.hasLongSum()
.points()
.satisfiesExactly(
point ->
assertThat(point)
.hasValue(0)
.attributes()
.containsOnly(attributeEntry("tag", "value")))));
// when
TimeUnit.MILLISECONDS.sleep(100);
sample.stop();
// then sample of a removed timer does not record any data
AbstractIterableAssert::isEmpty);
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testLongTaskTimerSeconds",
metrics ->
metrics.allSatisfy(
metric ->
assertThat(metric)
.hasDoubleHistogram()
.points()
.noneSatisfy(
point -> assertThat(point).hasSumGreaterThan(0.2).hasCount(2))));
"testLongTaskTimerSeconds.duration",
AbstractIterableAssert::isEmpty);
}
}

View File

@ -12,6 +12,7 @@ import io.micrometer.core.instrument.LongTaskTimer;
import io.micrometer.core.instrument.Metrics;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import java.util.concurrent.TimeUnit;
import org.assertj.core.api.AbstractIterableAssert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -58,31 +59,33 @@ public abstract class AbstractLongTaskTimerTest {
.hasValue(1)
.attributes()
.containsOnly(attributeEntry("tag", "value")))));
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testLongTaskTimer.duration",
metrics ->
metrics.anySatisfy(
metric ->
assertThat(metric)
.hasDescription("This is a test long task timer")
.hasUnit("ms")
.hasDoubleSum()
.isNotMonotonic()
.points()
.satisfiesExactly(
point -> {
assertThat(point)
.attributes()
.containsOnly(attributeEntry("tag", "value"));
// any value >0 - duration of currently running tasks
assertThat(point.getValue()).isPositive();
})));
// when
TimeUnit.MILLISECONDS.sleep(100);
sample.stop();
// then
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testLongTaskTimer",
metrics ->
metrics.anySatisfy(
metric ->
assertThat(metric)
.hasDescription("This is a test long task timer")
.hasUnit("ms")
.hasDoubleHistogram()
.points()
.satisfiesExactly(
point ->
assertThat(point)
.hasSumGreaterThan(100)
.hasCount(1)
.attributes()
.containsOnly(attributeEntry("tag", "value")))));
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
@ -99,76 +102,34 @@ public abstract class AbstractLongTaskTimerTest {
.hasValue(0)
.attributes()
.containsOnly(attributeEntry("tag", "value")))));
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testLongTaskTimer.duration",
metrics ->
metrics.anySatisfy(
metric ->
assertThat(metric)
.hasDoubleSum()
.points()
.satisfiesExactly(
point ->
assertThat(point)
.hasValue(0)
.attributes()
.containsOnly(attributeEntry("tag", "value")))));
testing().clearData();
// when timer is removed from the registry
Metrics.globalRegistry.remove(timer);
sample = timer.start();
timer.start();
// then no tasks are active after starting a new sample
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testLongTaskTimer.active",
metrics ->
metrics.anySatisfy(
metric ->
assertThat(metric)
.hasLongSum()
.points()
.satisfiesExactly(
point ->
assertThat(point)
.hasValue(0)
.attributes()
.containsOnly(attributeEntry("tag", "value")))));
// when
TimeUnit.MILLISECONDS.sleep(100);
sample.stop();
// then sample of a removed timer does not record any data
INSTRUMENTATION_NAME, "testLongTaskTimer.active", AbstractIterableAssert::isEmpty);
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testLongTaskTimer",
metrics ->
metrics.allSatisfy(
metric ->
assertThat(metric)
.hasDoubleHistogram()
.points()
.noneSatisfy(
point -> assertThat(point).hasSumGreaterThan(200).hasCount(2))));
}
@Test
void testMultipleSampleStopCalls() throws InterruptedException {
// given
LongTaskTimer timer =
LongTaskTimer.builder("testLongTaskTimerSampleStop").register(Metrics.globalRegistry);
// when stop() is called multiple times
LongTaskTimer.Sample sample = timer.start();
TimeUnit.MILLISECONDS.sleep(100);
sample.stop();
sample.stop();
sample.stop();
// then only the first time is recorded
testing()
.waitAndAssertMetrics(
INSTRUMENTATION_NAME,
"testLongTaskTimerSampleStop",
metrics ->
metrics.allSatisfy(
metric ->
assertThat(metric)
.hasDoubleHistogram()
.points()
.satisfiesExactly(
point -> assertThat(point).hasSumGreaterThan(100).hasCount(1))));
INSTRUMENTATION_NAME, "testLongTaskTimer.duration", AbstractIterableAssert::isEmpty);
}
}