From ab587a927a9c49d5e74c0742688103d4662de024 Mon Sep 17 00:00:00 2001 From: Mateusz Rzeszutek Date: Fri, 18 Mar 2022 13:03:42 +0100 Subject: [PATCH] Implement "Prometheus mode" for better micrometer->OTel->Prometheus support (#5537) * Implement "Prometheus mode" for better micrometer->OTel->Prometheus support * code review comments * forgot about README --- .../micrometer/micrometer-1.5/README.md | 3 +- .../micrometer-1.5/javaagent/build.gradle.kts | 10 + .../micrometer/v1_5/PrometheusModeTest.java | 22 ++ .../OpenTelemetryDistributionSummary.java | 5 +- .../v1_5/OpenTelemetryMeterRegistry.java | 9 +- .../OpenTelemetryMeterRegistryBuilder.java | 35 +- .../micrometer/v1_5/OpenTelemetryTimer.java | 2 +- .../v1_5/PrometheusModeNamingConvention.java | 36 ++ .../micrometer/v1_5/TimeUnitHelper.java | 11 +- .../micrometer/v1_5/PrometheusModeTest.java | 41 ++ .../v1_5/AbstractPrometheusModeTest.java | 358 ++++++++++++++++++ 11 files changed, 516 insertions(+), 16 deletions(-) create mode 100644 instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/PrometheusModeTest.java create mode 100644 instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/PrometheusModeNamingConvention.java create mode 100644 instrumentation/micrometer/micrometer-1.5/library/src/test/java/io/opentelemetry/instrumentation/micrometer/v1_5/PrometheusModeTest.java create mode 100644 instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractPrometheusModeTest.java diff --git a/instrumentation/micrometer/micrometer-1.5/README.md b/instrumentation/micrometer/micrometer-1.5/README.md index 2181acafa118..1bb868a05f81 100644 --- a/instrumentation/micrometer/micrometer-1.5/README.md +++ b/instrumentation/micrometer/micrometer-1.5/README.md @@ -2,4 +2,5 @@ | System property | Type | Default | Description | |---|---|---|---| -| `otel.instrumentation.micrometer.base-time-unit` | String | `ms` | Set the base time unit for the OpenTelemetry `MeterRegistry` implementation.
Valid values`ns`, `nanoseconds`, `us`, `microseconds`, `ms`, `microseconds`, `s`, `seconds`, `min`, `minutes`, `h`, `hours`, `d`, `days`
| \ No newline at end of file +| `otel.instrumentation.micrometer.base-time-unit` | String | `ms` | Set the base time unit for the OpenTelemetry `MeterRegistry` implementation.
Valid values`ns`, `nanoseconds`, `us`, `microseconds`, `ms`, `microseconds`, `s`, `seconds`, `min`, `minutes`, `h`, `hours`, `d`, `days`
| +| `otel.instrumentation.micrometer.prometheus-mode.enabled` | boolean | false | Enable the "Prometheus mode" this will simulate the behavior of Micrometer's PrometheusMeterRegistry. The instruments will be renamed to match Micrometer instrument naming, and the base time unit will be set to seconds. | diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/build.gradle.kts b/instrumentation/micrometer/micrometer-1.5/javaagent/build.gradle.kts index 7ec3a8bc7218..cc2ba4246dfe 100644 --- a/instrumentation/micrometer/micrometer-1.5/javaagent/build.gradle.kts +++ b/instrumentation/micrometer/micrometer-1.5/javaagent/build.gradle.kts @@ -20,6 +20,14 @@ dependencies { } tasks { + val testPrometheusMode by registering(Test::class) { + filter { + includeTestsMatching("*PrometheusModeTest") + } + include("**/*PrometheusModeTest.*") + jvmArgs("-Dotel.instrumentation.micrometer.prometheus-mode.enabled=true") + } + val testBaseTimeUnit by registering(Test::class) { filter { includeTestsMatching("*TimerSecondsTest") @@ -31,10 +39,12 @@ tasks { test { filter { excludeTestsMatching("*TimerSecondsTest") + excludeTestsMatching("*PrometheusModeTest") } } check { dependsOn(testBaseTimeUnit) + dependsOn(testPrometheusMode) } } diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/PrometheusModeTest.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/PrometheusModeTest.java new file mode 100644 index 000000000000..63768d280271 --- /dev/null +++ b/instrumentation/micrometer/micrometer-1.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/PrometheusModeTest.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.micrometer.v1_5; + +import io.opentelemetry.instrumentation.micrometer.v1_5.AbstractPrometheusModeTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class PrometheusModeTest extends AbstractPrometheusModeTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryDistributionSummary.java b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryDistributionSummary.java index 57d793ddcdc7..9fb1fed637d8 100644 --- a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryDistributionSummary.java +++ b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryDistributionSummary.java @@ -12,7 +12,6 @@ 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.config.NamingConvention; import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; @@ -27,7 +26,7 @@ import java.util.concurrent.atomic.LongAdder; final class OpenTelemetryDistributionSummary extends AbstractDistributionSummary - implements DistributionSummary, RemovableMeter { + implements RemovableMeter { private final Measurements measurements; private final TimeWindowMax max; @@ -78,7 +77,7 @@ boolean isUsingMicrometerHistograms() { @Override protected void recordNonNegative(double amount) { - if (amount >= 0 && !removed) { + if (!removed) { otelHistogram.record(amount, attributes); measurements.record(amount); max.record(amount); diff --git a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryMeterRegistry.java b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryMeterRegistry.java index f7b9d76f84a9..17556c0e2901 100644 --- a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryMeterRegistry.java +++ b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryMeterRegistry.java @@ -53,13 +53,18 @@ public static OpenTelemetryMeterRegistryBuilder builder(OpenTelemetry openTeleme private final io.opentelemetry.api.metrics.Meter otelMeter; OpenTelemetryMeterRegistry( - Clock clock, TimeUnit baseTimeUnit, io.opentelemetry.api.metrics.Meter otelMeter) { + Clock clock, + TimeUnit baseTimeUnit, + boolean prometheusMode, + io.opentelemetry.api.metrics.Meter otelMeter) { super(clock); this.baseTimeUnit = baseTimeUnit; this.otelMeter = otelMeter; + NamingConvention namingConvention = + prometheusMode ? PrometheusModeNamingConvention.INSTANCE : NamingConvention.identity; this.config() - .namingConvention(NamingConvention.identity) + .namingConvention(namingConvention) .onMeterRemoved(OpenTelemetryMeterRegistry::onMeterRemoved); } diff --git a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryMeterRegistryBuilder.java b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryMeterRegistryBuilder.java index 157549d9f695..13309b8a123d 100644 --- a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryMeterRegistryBuilder.java +++ b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryMeterRegistryBuilder.java @@ -10,6 +10,7 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.config.Config; import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; /** A builder of {@link OpenTelemetryMeterRegistry}. */ public final class OpenTelemetryMeterRegistryBuilder { @@ -18,9 +19,9 @@ public final class OpenTelemetryMeterRegistryBuilder { private final OpenTelemetry openTelemetry; private Clock clock = Clock.SYSTEM; - private TimeUnit baseTimeUnit = - TimeUnitHelper.parseConfigValue( - Config.get().getString("otel.instrumentation.micrometer.base-time-unit")); + @Nullable private TimeUnit baseTimeUnit = null; + private boolean prometheusMode = + Config.get().getBoolean("otel.instrumentation.micrometer.prometheus-mode.enabled", false); OpenTelemetryMeterRegistryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; @@ -38,12 +39,38 @@ public OpenTelemetryMeterRegistryBuilder setBaseTimeUnit(TimeUnit baseTimeUnit) return this; } + /** + * Enables the "Prometheus mode" - this will simulate the behavior of Micrometer's {@code + * PrometheusMeterRegistry}. The instruments will be renamed to match Micrometer instrument + * naming, and the base time unit will be set to seconds. + * + *

Set this to {@code true} if you are using the Prometheus metrics exporter. + */ + public OpenTelemetryMeterRegistryBuilder setPrometheusMode(boolean prometheusMode) { + this.prometheusMode = prometheusMode; + return this; + } + /** * Returns a new {@link OpenTelemetryMeterRegistry} with the settings of this {@link * OpenTelemetryMeterRegistryBuilder}. */ public MeterRegistry build() { + if (prometheusMode) { + // prometheus mode overrides any unit settings with SECONDS + setBaseTimeUnit(TimeUnit.SECONDS); + } else if (baseTimeUnit == null) { + // if the unit was not manually set, try to initialize it using config + setBaseTimeUnit( + TimeUnitHelper.parseConfigValue( + Config.get().getString("otel.instrumentation.micrometer.base-time-unit"), + TimeUnit.MILLISECONDS)); + } + return new OpenTelemetryMeterRegistry( - clock, baseTimeUnit, openTelemetry.getMeterProvider().get(INSTRUMENTATION_NAME)); + clock, + baseTimeUnit, + prometheusMode, + openTelemetry.getMeterProvider().get(INSTRUMENTATION_NAME)); } } diff --git a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryTimer.java b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryTimer.java index 6bee7a08dac8..457ef9d89887 100644 --- a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryTimer.java +++ b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryTimer.java @@ -81,7 +81,7 @@ boolean isUsingMicrometerHistograms() { @Override protected void recordNonNegative(long amount, TimeUnit unit) { - if (amount >= 0 && !removed) { + if (!removed) { long nanos = unit.toNanos(amount); double time = TimeUtils.nanosToUnit(nanos, baseTimeUnit); otelHistogram.record(time, attributes); diff --git a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/PrometheusModeNamingConvention.java b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/PrometheusModeNamingConvention.java new file mode 100644 index 000000000000..2a4143512756 --- /dev/null +++ b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/PrometheusModeNamingConvention.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.micrometer.v1_5; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.config.NamingConvention; +import javax.annotation.Nullable; + +// This naming strategy does not replace '.' with '_', and it does not append '_total' to counter +// names - the reason behind it is that this is already done by the Prometheus exporter; see the +// io.opentelemetry.exporter.prometheus.MetricAdapter class +enum PrometheusModeNamingConvention implements NamingConvention { + INSTANCE; + + @Override + public String name(String name, Meter.Type type, @Nullable String baseUnit) { + if (type == Meter.Type.COUNTER + || type == Meter.Type.DISTRIBUTION_SUMMARY + || type == Meter.Type.GAUGE) { + if (baseUnit != null && !name.endsWith("." + baseUnit)) { + name = name + "." + baseUnit; + } + } + + if (type == Meter.Type.LONG_TASK_TIMER || type == Meter.Type.TIMER) { + if (!name.endsWith(".seconds")) { + name = name + ".seconds"; + } + } + + return name; + } +} diff --git a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/TimeUnitHelper.java b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/TimeUnitHelper.java index 29af910c8827..1589e19834e3 100644 --- a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/TimeUnitHelper.java +++ b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/TimeUnitHelper.java @@ -15,9 +15,9 @@ final class TimeUnitHelper { private static final Logger logger = LoggerFactory.getLogger(OpenTelemetryMeterRegistry.class); - static TimeUnit parseConfigValue(@Nullable String value) { + static TimeUnit parseConfigValue(@Nullable String value, TimeUnit defaultUnit) { if (value == null) { - return TimeUnit.MILLISECONDS; + return defaultUnit; } // short names are UCUM names // long names are just TimeUnit values lowercased @@ -45,9 +45,10 @@ static TimeUnit parseConfigValue(@Nullable String value) { return TimeUnit.DAYS; default: logger.warn( - "Invalid base time unit: '{}'; using microseconds as the base time unit instead", - value); - return TimeUnit.MILLISECONDS; + "Invalid base time unit: '{}'; using '{}' as the base time unit instead", + value, + getUnitString(defaultUnit)); + return defaultUnit; } } diff --git a/instrumentation/micrometer/micrometer-1.5/library/src/test/java/io/opentelemetry/instrumentation/micrometer/v1_5/PrometheusModeTest.java b/instrumentation/micrometer/micrometer-1.5/library/src/test/java/io/opentelemetry/instrumentation/micrometer/v1_5/PrometheusModeTest.java new file mode 100644 index 000000000000..bd9f022e7ffb --- /dev/null +++ b/instrumentation/micrometer/micrometer-1.5/library/src/test/java/io/opentelemetry/instrumentation/micrometer/v1_5/PrometheusModeTest.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.micrometer.v1_5; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; + +class PrometheusModeTest extends AbstractPrometheusModeTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + static MeterRegistry otelMeterRegistry; + + @BeforeAll + public static void setUpRegistry() { + otelMeterRegistry = + OpenTelemetryMeterRegistry.builder(testing.getOpenTelemetry()) + .setPrometheusMode(true) + .build(); + Metrics.addRegistry(otelMeterRegistry); + } + + @AfterAll + public static void tearDownRegistry() { + Metrics.removeRegistry(otelMeterRegistry); + } + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractPrometheusModeTest.java b/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractPrometheusModeTest.java new file mode 100644 index 000000000000..0f813ac4e221 --- /dev/null +++ b/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractPrometheusModeTest.java @@ -0,0 +1,358 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.micrometer.v1_5; + +import static io.opentelemetry.sdk.testing.assertj.MetricAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.FunctionTimer; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.LongTaskTimer; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("PreferJavaTimeOverload") +public abstract class AbstractPrometheusModeTest { + + static final String INSTRUMENTATION_NAME = "io.opentelemetry.micrometer-1.5"; + + protected abstract InstrumentationExtension testing(); + + final TestTimer timerObj = new TestTimer(); + + @BeforeEach + void cleanupMeters() { + Metrics.globalRegistry.forEachMeter(Metrics.globalRegistry::remove); + } + + @Test + void testCounter() { + // given + Counter counter = + Counter.builder("testPrometheusCounter") + .description("This is a test counter") + .tags("tag", "value") + .baseUnit("items") + .register(Metrics.globalRegistry); + + // when + counter.increment(12); + + // then + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "testPrometheusCounter.items", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("This is a test counter") + .hasUnit("items") + .hasDoubleSum() + .isMonotonic() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasValue(12) + .attributes() + .containsOnly(attributeEntry("tag", "value"))))); + } + + @Test + void testDistributionSummary() { + // given + DistributionSummary summary = + DistributionSummary.builder("testPrometheusSummary") + .description("This is a test summary") + .baseUnit("items") + .tag("tag", "value") + .register(Metrics.globalRegistry); + + // when + summary.record(12); + summary.record(42); + + // then + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "testPrometheusSummary.items", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("This is a test summary") + .hasUnit("items") + .hasDoubleHistogram() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasSum(54) + .hasCount(2) + .attributes() + .containsOnly(attributeEntry("tag", "value"))))); + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "testPrometheusSummary.items.max", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("This is a test summary") + .hasUnit("items") + .hasDoubleGauge() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasValue(42) + .attributes() + .containsOnly(attributeEntry("tag", "value"))))); + } + + @Test + void testFunctionTimer() { + // given + FunctionTimer.builder( + "testPrometheusFunctionTimer", + timerObj, + TestTimer::getCount, + TestTimer::getTotalTimeNanos, + TimeUnit.NANOSECONDS) + .description("This is a test function timer") + .tags("tag", "value") + .register(Metrics.globalRegistry); + + // when + timerObj.add(42, TimeUnit.SECONDS); + + // then + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "testPrometheusFunctionTimer.seconds.count", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("This is a test function timer") + .hasUnit("1") + .hasLongSum() + .isMonotonic() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasValue(1) + .attributes() + .containsOnly(attributeEntry("tag", "value"))))); + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "testPrometheusFunctionTimer.seconds.sum", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("This is a test function timer") + .hasUnit("s") + .hasDoubleSum() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasValue(42) + .attributes() + .containsOnly(attributeEntry("tag", "value"))))); + } + + @Test + void testGauge() { + // when + Gauge.builder("testPrometheusGauge", () -> 42) + .description("This is a test gauge") + .tags("tag", "value") + .baseUnit("items") + .register(Metrics.globalRegistry); + + // then + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "testPrometheusGauge.items", + 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"))))); + } + + @Test + void testLongTaskTimer() throws InterruptedException { + // given + LongTaskTimer timer = + LongTaskTimer.builder("testPrometheusLongTaskTimer") + .description("This is a test long task timer") + .tags("tag", "value") + .register(Metrics.globalRegistry); + + // when + LongTaskTimer.Sample sample = timer.start(); + + // then + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "testPrometheusLongTaskTimer.seconds.active", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("This is a test long task timer") + .hasUnit("tasks") + .hasLongSum() + .isNotMonotonic() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasValue(1) + .attributes() + .containsOnly(attributeEntry("tag", "value"))))); + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "testPrometheusLongTaskTimer.seconds.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, + "testPrometheusLongTaskTimer.seconds.active", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasLongSum() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasValue(0) + .attributes() + .containsOnly(attributeEntry("tag", "value"))))); + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "testPrometheusLongTaskTimer.seconds.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDoubleSum() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasValue(0) + .attributes() + .containsOnly(attributeEntry("tag", "value"))))); + } + + @Test + void testTimer() { + // given + Timer timer = + Timer.builder("testPrometheusTimer") + .description("This is a test timer") + .tags("tag", "value") + .register(Metrics.globalRegistry); + + // when + timer.record(1, TimeUnit.SECONDS); + timer.record(5, TimeUnit.SECONDS); + timer.record(10_789, TimeUnit.MILLISECONDS); + + // then + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "testPrometheusTimer.seconds", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("This is a test timer") + .hasUnit("s") + .hasDoubleHistogram() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasSum(16.789) + .hasCount(3) + .attributes() + .containsOnly(attributeEntry("tag", "value"))))); + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "testPrometheusTimer.seconds.max", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("This is a test timer") + .hasUnit("s") + .hasDoubleGauge() + .points() + .satisfiesExactly( + point -> + assertThat(point) + .hasValue(10.789) + .attributes() + .containsOnly(attributeEntry("tag", "value"))))); + } +}