diff --git a/.github/component_owners.yml b/.github/component_owners.yml index a973b1b05..e660b4f69 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -29,6 +29,8 @@ components: maven-extension: - cyrille-leclerc - kenfinnigan + micrometer-meter-provider: + - HaloFour runtime-attach: - iNikem - trask diff --git a/micrometer-meter-provider/README.md b/micrometer-meter-provider/README.md new file mode 100644 index 000000000..abc6eaae4 --- /dev/null +++ b/micrometer-meter-provider/README.md @@ -0,0 +1,61 @@ +# Micrometer MeterProvider + +This utility provides an implementation of `MeterProvider` which wraps a Micrometer `MeterRegistry` +and delegates the reporting of all metrics through Micrometer. This enables projects which already +rely on Micrometer and cannot currently migrate to OpenTelemetry Metrics to be able to report on +metrics that are reported through the OpenTelemetry Metrics API. + +### Usage + +Create the `MicrometerMeterProvider` passing an existing instance of `MeterRegistry`. Then you can +use the OpenTelemetry Metrics `MeterProvider` API to create instruments. + +```java +MeterRegistry meterRegistry = ...; + +// create the meter provider +MeterProvider meterProvider = MicrometerMeterProvider.builder(meterRegistry) + .build(); +Meter meter = meterProvider.get("my-app"); + +// create an instrument +LongCounter counter = meter.counterBuilder("my.counter") + .build(); + +// record metrics +count.add(1, Attributes.of(AttributeKey.stringKey("key"), "value")); +``` + +**Note**: Instruments in OpenTelemetry are created without tags, which are reported with each +measurement. But tags are required to create Micrometer metrics. Because of this difference the +adapter must listen for when measurements are being read by the `MeterRegistry` in order to call +callbacks registered for observable metrics in order to create the Micrometer meters on demand. + +By default the `MicrometerMeterProvider` will create a dummy `Metric` with the name +"otel-polling-meter" which will be used to poll the asynchronous OpenTelemetry instruments as it +is measured. However, you can also specify an alternative `CallbackRegistrar` strategy. + +```java +MeterRegistry meterRegistry = ...; + +// create the meter provider +MeterProvider meterProvider = MicrometerMeterProvider.builder(meterRegistry) + .setCallbackRegistrar(ScheduledCallbackRegistrar.builder() + .setPeriod(Duration.ofSeconds(10L)) + .build()) + .build(); +Meter meter = meterProvider.get("my-app"); + +// create an asynchronous instrument +ObservableDoubleGauge gauge = meter.gaugeBuilder("my.gauge") + .buildWithCallback(measurement -> { + // record metrics + measurement.record(queue.size(), Attributes.of(AttributeKey.stringKey("key"), "value")); + }); +``` + +## Component owners + +- [Justin Spindler](https://github.com/HaloFour), Comcast + +Learn more about component owners in [component_owners.yml](../.github/component_owners.yml). diff --git a/micrometer-meter-provider/build.gradle.kts b/micrometer-meter-provider/build.gradle.kts new file mode 100644 index 000000000..b95dede42 --- /dev/null +++ b/micrometer-meter-provider/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + id("otel.java-conventions") + id("otel.publish-conventions") +} + +description = "OpenTelemetry Micrometer MeterProvider" + +dependencies { + api("io.opentelemetry:opentelemetry-api") + api("io.opentelemetry:opentelemetry-sdk-metrics") + + compileOnly("io.micrometer:micrometer-core:1.1.0") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + + annotationProcessor("com.google.auto.service:auto-service") + compileOnly("com.google.auto.service:auto-service-annotations") + + annotationProcessor("com.google.auto.value:auto-value") + compileOnly("com.google.auto.value:auto-value-annotations") + + testImplementation("io.micrometer:micrometer-core:1.8.5") +} + +testing { + suites { + val integrationTest by registering(JvmTestSuite::class) { + dependencies { + implementation("io.micrometer:micrometer-registry-prometheus:1.8.5") + } + } + } +} diff --git a/micrometer-meter-provider/src/integrationTest/java/io/opentelemetry/contrib/metrics/micrometer/PrometheusIntegrationTest.java b/micrometer-meter-provider/src/integrationTest/java/io/opentelemetry/contrib/metrics/micrometer/PrometheusIntegrationTest.java new file mode 100644 index 000000000..4b0ec0d8c --- /dev/null +++ b/micrometer-meter-provider/src/integrationTest/java/io/opentelemetry/contrib/metrics/micrometer/PrometheusIntegrationTest.java @@ -0,0 +1,449 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.micrometer.prometheus.PrometheusConfig; +import io.micrometer.prometheus.PrometheusMeterRegistry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleUpDownCounter; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.api.metrics.ObservableDoubleCounter; +import io.opentelemetry.api.metrics.ObservableDoubleGauge; +import io.opentelemetry.api.metrics.ObservableDoubleUpDownCounter; +import io.opentelemetry.api.metrics.ObservableLongCounter; +import io.opentelemetry.api.metrics.ObservableLongGauge; +import io.opentelemetry.api.metrics.ObservableLongUpDownCounter; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class PrometheusIntegrationTest { + private static final AttributeKey KEY1 = AttributeKey.stringKey("key1"); + + PrometheusMeterRegistry prometheusMeterRegistry; + + Meter meter; + + @BeforeEach + void setUp() { + prometheusMeterRegistry = new PrometheusMeterRegistry(DefaultPrometheusConfig.INSTANCE); + MeterProvider meterProvider = MicrometerMeterProvider.builder(prometheusMeterRegistry).build(); + meter = meterProvider.meterBuilder("integrationTest").setInstrumentationVersion("1.0").build(); + } + + @Test + void noMeters() { + String output = prometheusMeterRegistry.scrape(); + + assertThat(output).isEmpty(); + } + + @Test + void longCounter() { + LongCounter longCounter = + meter + .counterBuilder("longCounter") + .setDescription("LongCounter test") + .setUnit("units") + .build(); + + longCounter.add(1, Attributes.of(KEY1, "value1")); + longCounter.add(2, Attributes.of(KEY1, "value2")); + + String output = prometheusMeterRegistry.scrape(); + + assertThat(output) + .contains("# HELP longCounter_units_total LongCounter test") + .contains("# TYPE longCounter_units_total counter") + .contains( + "longCounter_units_total{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.0") + .contains( + "longCounter_units_total{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.0"); + } + + @Test + void observableLongCounter() { + try (ObservableLongCounter observableLongCounter = + meter + .counterBuilder("longCounter") + .setDescription("LongCounter test") + .setUnit("units") + .buildWithCallback( + onlyOnce( + measurement -> { + measurement.record(1, Attributes.of(KEY1, "value1")); + measurement.record(2, Attributes.of(KEY1, "value2")); + }))) { + + String output = scrapeFor("longCounter"); + assertThat(output) + .contains("# HELP otel_polling_meter") + .contains("# TYPE otel_polling_meter untyped") + .contains("# TYPE longCounter_units_total counter") + .contains("# HELP longCounter_units_total LongCounter test") + .contains( + "longCounter_units_total{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.0") + .contains( + "longCounter_units_total{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.0"); + } + } + + @Test + void doubleCounter() { + DoubleCounter doubleCounter = + meter + .counterBuilder("doubleCounter") + .ofDoubles() + .setDescription("DoubleCounter test") + .setUnit("units") + .build(); + + doubleCounter.add(1.5, Attributes.of(KEY1, "value1")); + doubleCounter.add(2.5, Attributes.of(KEY1, "value2")); + + String output = prometheusMeterRegistry.scrape(); + + assertThat(output) + .contains("# HELP doubleCounter_units_total DoubleCounter test") + .contains("# TYPE doubleCounter_units_total counter") + .contains( + "doubleCounter_units_total{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.5") + .contains( + "doubleCounter_units_total{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.5"); + } + + @Test + void observableDoubleCounter() { + try (ObservableDoubleCounter observableDoubleCounter = + meter + .counterBuilder("doubleCounter") + .ofDoubles() + .setDescription("DoubleCounter test") + .setUnit("units") + .buildWithCallback( + onlyOnce( + measurement -> { + measurement.record(1.5, Attributes.of(KEY1, "value1")); + measurement.record(2.5, Attributes.of(KEY1, "value2")); + }))) { + + String output = scrapeFor("doubleCounter"); + assertThat(output) + .contains("# HELP otel_polling_meter") + .contains("# TYPE otel_polling_meter untyped") + .contains("# TYPE doubleCounter_units_total counter") + .contains("# HELP doubleCounter_units_total DoubleCounter test") + .contains( + "doubleCounter_units_total{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.5") + .contains( + "doubleCounter_units_total{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.5"); + } + } + + @Test + void longUpDownCounter() { + LongUpDownCounter longUpDownCounter = + meter + .upDownCounterBuilder("longUpDownCounter") + .setDescription("LongUpDownCounter test") + .setUnit("units") + .build(); + + longUpDownCounter.add(1, Attributes.of(KEY1, "value1")); + longUpDownCounter.add(2, Attributes.of(KEY1, "value2")); + + String output = prometheusMeterRegistry.scrape(); + + assertThat(output) + .contains("# HELP longUpDownCounter_units LongUpDownCounter test") + .contains("# TYPE longUpDownCounter_units gauge") + .contains( + "longUpDownCounter_units{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.0") + .contains( + "longUpDownCounter_units{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.0"); + + longUpDownCounter.add(1, Attributes.of(KEY1, "value1")); + longUpDownCounter.add(2, Attributes.of(KEY1, "value2")); + + output = prometheusMeterRegistry.scrape(); + + assertThat(output) + .contains( + "longUpDownCounter_units{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.0") + .contains( + "longUpDownCounter_units{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 4.0"); + } + + @Test + void observableLongUpDownCounter() { + try (ObservableLongUpDownCounter observableLongUpDownCounter = + meter + .upDownCounterBuilder("longUpDownCounter") + .setDescription("LongUpDownCounter test") + .setUnit("units") + .buildWithCallback( + measurement -> { + measurement.record(1, Attributes.of(KEY1, "value1")); + measurement.record(-2, Attributes.of(KEY1, "value2")); + })) { + + String output = scrapeFor("longUpDownCounter"); + assertThat(output) + .contains("# HELP otel_polling_meter") + .contains("# TYPE otel_polling_meter untyped") + .contains("# TYPE longUpDownCounter_units gauge") + .contains("# HELP longUpDownCounter_units LongUpDownCounter test") + .contains( + "longUpDownCounter_units{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.0") + .contains( + "longUpDownCounter_units{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} -2.0"); + } + } + + @Test + void doubleUpDownCounter() { + DoubleUpDownCounter doubleUpDownCounter = + meter + .upDownCounterBuilder("doubleUpDownCounter") + .ofDoubles() + .setDescription("DoubleUpDownCounter test") + .setUnit("units") + .build(); + + doubleUpDownCounter.add(1.5, Attributes.of(KEY1, "value1")); + doubleUpDownCounter.add(2.5, Attributes.of(KEY1, "value2")); + + String output = prometheusMeterRegistry.scrape(); + + assertThat(output) + .contains("# HELP doubleUpDownCounter_units DoubleUpDownCounter test") + .contains("# TYPE doubleUpDownCounter_units gauge") + .contains( + "doubleUpDownCounter_units{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.5") + .contains( + "doubleUpDownCounter_units{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.5"); + + doubleUpDownCounter.add(0.5, Attributes.of(KEY1, "value1")); + doubleUpDownCounter.add(-1.5, Attributes.of(KEY1, "value2")); + + output = prometheusMeterRegistry.scrape(); + + assertThat(output) + .contains( + "doubleUpDownCounter_units{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.0") + .contains( + "doubleUpDownCounter_units{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.0"); + } + + @Test + void observableDoubleUpDownCounter() { + try (ObservableDoubleUpDownCounter observableDoubleUpDownCounter = + meter + .upDownCounterBuilder("doubleUpDownCounter") + .ofDoubles() + .setDescription("DoubleUpDownCounter test") + .setUnit("units") + .buildWithCallback( + onlyOnce( + measurement -> { + measurement.record(1.5, Attributes.of(KEY1, "value1")); + measurement.record(-2.5, Attributes.of(KEY1, "value2")); + }))) { + + String output = scrapeFor("doubleUpDownCounter"); + assertThat(output) + .contains("# HELP otel_polling_meter") + .contains("# TYPE otel_polling_meter untyped") + .contains("# TYPE doubleUpDownCounter_units gauge") + .contains("# HELP doubleUpDownCounter_units DoubleUpDownCounter test") + .contains( + "doubleUpDownCounter_units{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.5") + .contains( + "doubleUpDownCounter_units{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} -2.5"); + } + } + + @Test + void doubleHistogram() { + DoubleHistogram doubleHistogram = + meter + .histogramBuilder("doubleHistogram") + .setDescription("DoubleHistogram test") + .setUnit("units") + .build(); + + doubleHistogram.record(1.5, Attributes.of(KEY1, "value1")); + doubleHistogram.record(2.5, Attributes.of(KEY1, "value2")); + + String output = prometheusMeterRegistry.scrape(); + assertThat(output) + .contains("# HELP doubleHistogram_units DoubleHistogram test") + .contains("# TYPE doubleHistogram_units summary") + .contains( + "doubleHistogram_units_count{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.0") + .contains( + "doubleHistogram_units_sum{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.5") + .contains( + "doubleHistogram_units_count{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.0") + .contains( + "doubleHistogram_units_sum{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.5") + .contains("# HELP doubleHistogram_units_max DoubleHistogram test") + .contains("# TYPE doubleHistogram_units_max gauge") + .contains( + "doubleHistogram_units_max{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.5") + .contains( + "doubleHistogram_units_max{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.5"); + + doubleHistogram.record(2.5, Attributes.of(KEY1, "value1")); + + output = prometheusMeterRegistry.scrape(); + assertThat(output) + .contains( + "doubleHistogram_units_count{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.0") + .contains( + "doubleHistogram_units_sum{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 4.0") + .contains( + "doubleHistogram_units_max{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.5"); + } + + @Test + void longHistogram() { + LongHistogram longHistogram = + meter + .histogramBuilder("longHistogram") + .ofLongs() + .setDescription("LongHistogram test") + .setUnit("units") + .build(); + + longHistogram.record(1, Attributes.of(KEY1, "value1")); + longHistogram.record(2, Attributes.of(KEY1, "value2")); + + String output = prometheusMeterRegistry.scrape(); + assertThat(output) + .contains("# HELP longHistogram_units LongHistogram test") + .contains("# TYPE longHistogram_units summary") + .contains( + "longHistogram_units_count{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.0") + .contains( + "longHistogram_units_sum{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.0") + .contains( + "longHistogram_units_count{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.0") + .contains( + "longHistogram_units_sum{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.0") + .contains("# HELP longHistogram_units_max LongHistogram test") + .contains("# TYPE longHistogram_units_max gauge") + .contains( + "longHistogram_units_max{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.0") + .contains( + "longHistogram_units_max{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.0"); + + longHistogram.record(2, Attributes.of(KEY1, "value1")); + + output = prometheusMeterRegistry.scrape(); + assertThat(output) + .contains( + "longHistogram_units_count{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.0") + .contains( + "longHistogram_units_sum{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 3.0") + .contains( + "longHistogram_units_max{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.0"); + } + + @Test + void observableDoubleGauge() { + try (ObservableDoubleGauge observableDoubleGauge = + meter + .gaugeBuilder("doubleGauge") + .setDescription("DoubleGauge test") + .setUnit("units") + .buildWithCallback( + measurement -> { + measurement.record(1.5, Attributes.of(KEY1, "value1")); + measurement.record(2.5, Attributes.of(KEY1, "value2")); + })) { + + String output = scrapeFor("doubleGauge"); + assertThat(output) + .contains("# HELP otel_polling_meter") + .contains("# TYPE otel_polling_meter untyped") + .contains("# TYPE doubleGauge_units gauge") + .contains("# HELP doubleGauge_units DoubleGauge test") + .contains( + "doubleGauge_units{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.5") + .contains( + "doubleGauge_units{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.5"); + } + } + + @Test + void observableLongGauge() { + try (ObservableLongGauge observableLongGauge = + meter + .gaugeBuilder("longGauge") + .ofLongs() + .setDescription("LongGauge test") + .setUnit("units") + .buildWithCallback( + measurement -> { + measurement.record(1, Attributes.of(KEY1, "value1")); + measurement.record(2, Attributes.of(KEY1, "value2")); + })) { + + String output = scrapeFor("longGauge"); + assertThat(output) + .contains("# HELP otel_polling_meter") + .contains("# TYPE otel_polling_meter untyped") + .contains("# TYPE longGauge_units gauge") + .contains("# HELP longGauge_units LongGauge test") + .contains( + "longGauge_units{key1=\"value1\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 1.0") + .contains( + "longGauge_units{key1=\"value2\",otel_instrumentation_name=\"integrationTest\",otel_instrumentation_version=\"1.0\",} 2.0"); + } + } + + private String scrapeFor(String value) { + String output = prometheusMeterRegistry.scrape(); + if (!output.contains(value)) { + output = prometheusMeterRegistry.scrape(); + } + assertThat(output).contains(value); + return output; + } + + private Consumer onlyOnce(Consumer consumer) { + return new Consumer() { + final AtomicBoolean first = new AtomicBoolean(true); + + @Override + public void accept(T t) { + if (first.compareAndSet(true, false)) { + consumer.accept(t); + } + } + }; + } + + enum DefaultPrometheusConfig implements PrometheusConfig { + INSTANCE; + + @Override + public String get(String key) { + return null; + } + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/CallbackRegistrar.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/CallbackRegistrar.java new file mode 100644 index 000000000..7043e801d --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/CallbackRegistrar.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer; + +public interface CallbackRegistrar extends AutoCloseable { + CallbackRegistration registerCallback(Runnable runnable); + + @Override + void close(); +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/CallbackRegistration.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/CallbackRegistration.java new file mode 100644 index 000000000..3768e0d4a --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/CallbackRegistration.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer; + +import io.opentelemetry.api.metrics.ObservableDoubleCounter; +import io.opentelemetry.api.metrics.ObservableDoubleGauge; +import io.opentelemetry.api.metrics.ObservableDoubleUpDownCounter; +import io.opentelemetry.api.metrics.ObservableLongCounter; +import io.opentelemetry.api.metrics.ObservableLongGauge; +import io.opentelemetry.api.metrics.ObservableLongUpDownCounter; + +/** Helper interface representing any of the observable instruments. */ +@FunctionalInterface +public interface CallbackRegistration + extends ObservableLongCounter, + ObservableDoubleCounter, + ObservableLongUpDownCounter, + ObservableDoubleUpDownCounter, + ObservableLongGauge, + ObservableDoubleGauge, + AutoCloseable { + + /** + * Remove the callback registered via {@code buildWithCallback(Consumer)}. After this is called, + * the callback won't be invoked on future collections. Subsequent calls to {@link #close()} have + * no effect. + */ + @Override + void close(); +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeter.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeter.java new file mode 100644 index 000000000..e8a6b827c --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeter.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer; + +import io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.contrib.metrics.micrometer.internal.instruments.MicrometerDoubleGauge; +import io.opentelemetry.contrib.metrics.micrometer.internal.instruments.MicrometerDoubleHistogram; +import io.opentelemetry.contrib.metrics.micrometer.internal.instruments.MicrometerLongCounter; +import io.opentelemetry.contrib.metrics.micrometer.internal.instruments.MicrometerLongUpDownCounter; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import java.util.Objects; + +final class MicrometerMeter implements Meter { + final MeterSharedState meterSharedState; + + MicrometerMeter(MeterSharedState meterSharedState) { + this.meterSharedState = meterSharedState; + } + + @Override + public LongCounterBuilder counterBuilder(String name) { + Objects.requireNonNull(name, "name"); + return MicrometerLongCounter.builder(meterSharedState, name); + } + + @Override + public LongUpDownCounterBuilder upDownCounterBuilder(String name) { + Objects.requireNonNull(name, "name"); + return MicrometerLongUpDownCounter.builder(meterSharedState, name); + } + + @Override + public DoubleHistogramBuilder histogramBuilder(String name) { + Objects.requireNonNull(name, "name"); + return MicrometerDoubleHistogram.builder(meterSharedState, name); + } + + @Override + public DoubleGaugeBuilder gaugeBuilder(String name) { + Objects.requireNonNull(name, "name"); + return MicrometerDoubleGauge.builder(meterSharedState, name); + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeterBuilder.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeterBuilder.java new file mode 100644 index 000000000..20638c7c8 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeterBuilder.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer; + +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterBuilder; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterProviderSharedState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import javax.annotation.Nullable; + +final class MicrometerMeterBuilder implements MeterBuilder { + private final MeterProviderSharedState meterProviderSharedState; + private final String instrumentationScopeName; + @Nullable private String instrumentationScopeVersion; + @Nullable private String schemaUrl; + + MicrometerMeterBuilder( + MeterProviderSharedState meterProviderSharedState, String instrumentationScopeName) { + this.meterProviderSharedState = meterProviderSharedState; + this.instrumentationScopeName = instrumentationScopeName; + } + + @Override + public MeterBuilder setSchemaUrl(String schemaUrl) { + this.schemaUrl = schemaUrl; + return this; + } + + @Override + public MeterBuilder setInstrumentationVersion(String instrumentationScopeVersion) { + this.instrumentationScopeVersion = instrumentationScopeVersion; + return this; + } + + @Override + public Meter build() { + MeterSharedState state = + new MeterSharedState( + meterProviderSharedState, + instrumentationScopeName, + instrumentationScopeVersion, + schemaUrl); + return new MicrometerMeter(state); + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeterProvider.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeterProvider.java new file mode 100644 index 000000000..d76e19a34 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeterProvider.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer; + +import io.micrometer.core.instrument.MeterRegistry; +import io.opentelemetry.api.metrics.MeterBuilder; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.contrib.metrics.micrometer.internal.MemoizingSupplier; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterProviderSharedState; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * An implementation of {@link MeterProvider} that delegates metrics to a Micrometer {@link + * MeterRegistry}. + */ +public final class MicrometerMeterProvider implements MeterProvider, AutoCloseable { + + private final MeterProviderSharedState meterProviderSharedState; + private final CallbackRegistrar callbackRegistrar; + + /** + * Creates a new instance of {@link MicrometerMeterProvider} for the provided {@link + * MeterRegistry}. + * + * @param meterRegistrySupplier supplies the {@link MeterRegistry} + */ + MicrometerMeterProvider( + Supplier meterRegistrySupplier, CallbackRegistrar callbackRegistrar) { + this.callbackRegistrar = callbackRegistrar; + this.meterProviderSharedState = + new MeterProviderSharedState(meterRegistrySupplier, callbackRegistrar); + } + + /** Closes the current provider. */ + @Override + public void close() { + callbackRegistrar.close(); + } + + /** {@inheritDoc} */ + @Override + public MeterBuilder meterBuilder(String instrumentationScopeName) { + Objects.requireNonNull(instrumentationScopeName, "instrumentationScopeName"); + return new MicrometerMeterBuilder(meterProviderSharedState, instrumentationScopeName); + } + + /** Returns a new builder instance for this provider with the specified {@link MeterRegistry}. */ + public static MicrometerMeterProviderBuilder builder(MeterRegistry meterRegistry) { + Objects.requireNonNull(meterRegistry, "meterRegistry"); + return new MicrometerMeterProviderBuilder(() -> meterRegistry); + } + + /** + * Returns a new builder instance for this provider with a {@link Supplier} for a {@link + * MeterRegistry}. + * + *

This method should be used when the {@link MeterRegistry} must be lazily initialized. + */ + public static MicrometerMeterProviderBuilder builder( + Supplier meterRegistrySupplier) { + Objects.requireNonNull(meterRegistrySupplier, "meterRegistrySupplier"); + return new MicrometerMeterProviderBuilder(new MemoizingSupplier<>(meterRegistrySupplier)); + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeterProviderBuilder.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeterProviderBuilder.java new file mode 100644 index 000000000..c8f6cd96f --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeterProviderBuilder.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer; + +import io.micrometer.core.instrument.MeterRegistry; +import io.opentelemetry.contrib.metrics.micrometer.internal.PollingMeterCallbackRegistrar; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** Builder utility class for creating instances of {@link MicrometerMeterProvider}. */ +public class MicrometerMeterProviderBuilder { + private final Supplier meterRegistrySupplier; + @Nullable private CallbackRegistrar callbackRegistrar; + + MicrometerMeterProviderBuilder(Supplier meterRegistrySupplier) { + this.meterRegistrySupplier = meterRegistrySupplier; + } + + /** + * Sets the {@link CallbackRegistrar} used to poll asynchronous instruments for measurements. + * + *

If this is not set the {@link MicrometerMeterProvider} will create a {@link + * io.micrometer.core.instrument.Meter} which will poll asynchronous instruments when that meter + * is measured. + */ + public MicrometerMeterProviderBuilder setCallbackRegistrar(CallbackRegistrar callbackRegistrar) { + this.callbackRegistrar = callbackRegistrar; + return this; + } + + /** + * Constructs a new instance of the provider based on the builder's values. + * + * @return a new provider's instance. + */ + public MicrometerMeterProvider build() { + CallbackRegistrar callbackRegistrar = this.callbackRegistrar; + if (callbackRegistrar == null) { + callbackRegistrar = new PollingMeterCallbackRegistrar(meterRegistrySupplier); + } + return new MicrometerMeterProvider(meterRegistrySupplier, callbackRegistrar); + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/ScheduledCallbackRegistrar.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/ScheduledCallbackRegistrar.java new file mode 100644 index 000000000..d9021a234 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/ScheduledCallbackRegistrar.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; + +/** + * An implementation of {@link CallbackRegistrar} that uses a {@link ScheduledExecutorService} to + * execute the registered callbacks on the specified schedule. + */ +public final class ScheduledCallbackRegistrar implements CallbackRegistrar { + private final ScheduledExecutorService scheduledExecutorService; + private final long period; + private final TimeUnit timeUnit; + private final boolean ownsExecutor; + private final List callbacks; + @Nullable private volatile ScheduledFuture scheduledFuture; + + ScheduledCallbackRegistrar( + ScheduledExecutorService scheduledExecutorService, + long period, + TimeUnit timeUnit, + boolean ownsExecutor) { + this.scheduledExecutorService = scheduledExecutorService; + this.period = period; + this.timeUnit = timeUnit; + this.ownsExecutor = ownsExecutor; + this.callbacks = new CopyOnWriteArrayList<>(); + } + + public static ScheduledCallbackRegistrarBuilder builder( + ScheduledExecutorService scheduledExecutorService) { + Objects.requireNonNull(scheduledExecutorService, "scheduledExecutorService"); + return new ScheduledCallbackRegistrarBuilder(scheduledExecutorService); + } + + @Override + public CallbackRegistration registerCallback(Runnable callback) { + if (callback != null) { + ensureScheduled(); + callbacks.add(callback); + return () -> callbacks.remove(callback); + } else { + return () -> {}; + } + } + + private synchronized void ensureScheduled() { + if (scheduledFuture == null) { + scheduledFuture = + scheduledExecutorService.scheduleAtFixedRate(this::poll, period, period, timeUnit); + } + } + + private void poll() { + for (Runnable callback : callbacks) { + callback.run(); + } + } + + @Override + public synchronized void close() { + if (scheduledFuture != null) { + scheduledFuture.cancel(false); + scheduledFuture = null; + } + if (ownsExecutor) { + scheduledExecutorService.shutdown(); + } + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/ScheduledCallbackRegistrarBuilder.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/ScheduledCallbackRegistrarBuilder.java new file mode 100644 index 000000000..64de2f629 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/ScheduledCallbackRegistrarBuilder.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer; + +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** Builder utility class for creating instances of {@link ScheduledCallbackRegistrar}. */ +public final class ScheduledCallbackRegistrarBuilder { + private final ScheduledExecutorService scheduledExecutorService; + private long period; + private TimeUnit timeUnit; + private boolean shutdownExecutorOnClose; + + ScheduledCallbackRegistrarBuilder(ScheduledExecutorService scheduledExecutorService) { + this.scheduledExecutorService = scheduledExecutorService; + this.period = 1L; + this.timeUnit = TimeUnit.SECONDS; + } + + /** Sets the period between successive executions of each registered callback */ + public ScheduledCallbackRegistrarBuilder setPeriod(long period, TimeUnit unit) { + Objects.requireNonNull(unit, "unit"); + this.period = period; + this.timeUnit = unit; + return this; + } + + /** Sets the period between successive executions of each registered callback */ + public ScheduledCallbackRegistrarBuilder setPeriod(Duration period) { + Objects.requireNonNull(period, "period"); + this.period = period.toMillis(); + this.timeUnit = TimeUnit.MILLISECONDS; + return this; + } + + /** + * Sets that the executor should be {@link ScheduledExecutorService#shutdown() shutdown} when the + * {@link CallbackRegistrar} is {@link CallbackRegistrar#close() closed}. + */ + public ScheduledCallbackRegistrarBuilder setShutdownExecutorOnClose( + boolean shutdownExecutorOnClose) { + this.shutdownExecutorOnClose = shutdownExecutorOnClose; + return this; + } + + /** + * Constructs a new instance of the {@link CallbackRegistrar} based on the builder's values. + * + * @return a new instance. + */ + public CallbackRegistrar build() { + return new ScheduledCallbackRegistrar( + scheduledExecutorService, period, timeUnit, shutdownExecutorOnClose); + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/Constants.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/Constants.java new file mode 100644 index 000000000..50ed45da6 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/Constants.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal; + +import io.micrometer.core.instrument.Tag; + +/** + * Constants for common Micrometer {@link Tag} names for the OpenTelemetry instrumentation scope. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class Constants { + public static final String OTEL_INSTRUMENTATION_NAME = "otel.instrumentation.name"; + public static final String OTEL_INSTRUMENTATION_VERSION = "otel.instrumentation.version"; + public static final Tag UNKNOWN_INSTRUMENTATION_VERSION_TAG = + Tag.of(OTEL_INSTRUMENTATION_VERSION, "unknown"); + + private Constants() {} +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/MemoizingSupplier.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/MemoizingSupplier.java new file mode 100644 index 000000000..33bbc0aac --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/MemoizingSupplier.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal; + +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** + * Delegating implementation of {@link Supplier Supplier} that ensures that the {@link + * Supplier#get()} method is called at most once and the result is memoized for subsequent + * invocations. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class MemoizingSupplier implements Supplier { + private final Supplier delegate; + private volatile boolean initialized; + @Nullable private volatile T cachedResult; + + public MemoizingSupplier(Supplier delegate) { + this.delegate = delegate; + } + + @Override + @SuppressWarnings("NullAway") + public T get() { + T result = cachedResult; + if (!initialized) { + synchronized (this) { + if (!initialized) { + result = delegate.get(); + cachedResult = result; + initialized = true; + return result; + } + } + } + return result; + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/PollingMeterCallbackRegistrar.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/PollingMeterCallbackRegistrar.java new file mode 100644 index 000000000..935a81009 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/PollingMeterCallbackRegistrar.java @@ -0,0 +1,111 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.opentelemetry.contrib.metrics.micrometer.CallbackRegistrar; +import io.opentelemetry.contrib.metrics.micrometer.CallbackRegistration; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** + * Implementation of a {@link CallbackRegistrar} that uses a Micrometer {@link Meter} to invoke the + * callbacks when the meter is measured. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class PollingMeterCallbackRegistrar implements CallbackRegistrar { + static final String OTEL_POLLING_METER_NAME = "otel_polling_meter"; + + private final Supplier meterRegistrySupplier; + private final List callbacks; + @Nullable private volatile Meter pollingMeter; + + public PollingMeterCallbackRegistrar(Supplier meterRegistrySupplier) { + this.meterRegistrySupplier = meterRegistrySupplier; + this.callbacks = new CopyOnWriteArrayList<>(); + } + + private void poll() { + for (Runnable callback : this.callbacks) { + callback.run(); + } + } + + @Override + public CallbackRegistration registerCallback(Runnable callback) { + if (callback != null) { + ensurePollingMeterCreated(); + callbacks.add(callback); + return () -> callbacks.remove(callback); + } else { + return () -> {}; + } + } + + private synchronized void ensurePollingMeterCreated() { + if (pollingMeter == null) { + pollingMeter = createPollingMeter(meterRegistrySupplier.get()); + } + } + + /** + * Creates a dummy {@link Meter} which will be used to intercept when measurements are being + * enumerated so that observable instruments can be polled to record their metrics. + */ + private Meter createPollingMeter(MeterRegistry meterRegistry) { + return Meter.builder(OTEL_POLLING_METER_NAME, Meter.Type.OTHER, PollingIterable.of(this::poll)) + .register(meterRegistry); + } + + @Override + public synchronized void close() { + if (pollingMeter != null) { + meterRegistrySupplier.get().remove(pollingMeter); + pollingMeter = null; + } + } + + /** + * An implementation of an {@link Iterable Iterable} that will invoke a {@link Runnable} when + * it is enumerated. + */ + @SuppressWarnings("IterableAndIterator") + private static class PollingIterable implements Iterable, Iterator { + + static Iterable of(Runnable callback) { + return new PollingIterable<>(callback); + } + + private final Runnable callback; + + public PollingIterable(Runnable callback) { + this.callback = callback; + } + + @Override + public Iterator iterator() { + return this; + } + + @Override + public boolean hasNext() { + callback.run(); + return false; + } + + @Override + public T next() { + throw new NoSuchElementException(); + } + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractCounter.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractCounter.java new file mode 100644 index 000000000..608438169 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractCounter.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.FunctionCounter; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.InstrumentState; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +abstract class AbstractCounter extends AbstractInstrument { + private final Map counterMap = new ConcurrentHashMap<>(); + + protected AbstractCounter(InstrumentState instrumentState) { + super(instrumentState); + } + + protected final Counter counter(Attributes attributes) { + return Counter.builder(name()) + .tags(attributesToTags(attributes)) + .description(description()) + .baseUnit(unit()) + .register(meterRegistry()); + } + + protected final void record(double value, Attributes attributes) { + counterMap + .computeIfAbsent(attributesOrEmpty(attributes), this::createAsyncCounter) + .setMonotonically(value); + } + + private AtomicDoubleCounter createAsyncCounter(Attributes attributes) { + AtomicDoubleCounter counter = new AtomicDoubleCounter(); + FunctionCounter.builder(name(), counter, AtomicDoubleCounter::current) + .description(description()) + .baseUnit(unit()) + .tags(attributesToTags(attributes)) + .register(meterRegistry()); + return counter; + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractGauge.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractGauge.java new file mode 100644 index 000000000..bb1266424 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractGauge.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import io.micrometer.core.instrument.Gauge; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.InstrumentState; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +abstract class AbstractGauge extends AbstractInstrument { + private final Map gaugeMap = new ConcurrentHashMap<>(); + + protected AbstractGauge(InstrumentState instrumentState) { + super(instrumentState); + } + + protected final void record(double value, Attributes attributes) { + gaugeMap.computeIfAbsent(attributesOrEmpty(attributes), this::createAsyncGauge).set(value); + } + + private AtomicDoubleCounter createAsyncGauge(Attributes attributes) { + AtomicDoubleCounter counter = new AtomicDoubleCounter(); + Gauge.builder(name(), counter, AtomicDoubleCounter::current) + .description(description()) + .baseUnit(unit()) + .tags(attributesToTags(attributes)) + .register(meterRegistry()); + return counter; + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractHistogram.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractHistogram.java new file mode 100644 index 000000000..6c6ce2667 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractHistogram.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import io.micrometer.core.instrument.DistributionSummary; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.InstrumentState; + +abstract class AbstractHistogram extends AbstractInstrument { + protected AbstractHistogram(InstrumentState instrumentState) { + super(instrumentState); + } + + public DistributionSummary distribution(Attributes attributes) { + return DistributionSummary.builder(name()) + .tags(attributesToTags(attributes)) + .description(description()) + .baseUnit(unit()) + .register(meterRegistry()); + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractInstrument.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractInstrument.java new file mode 100644 index 000000000..05994034c --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractInstrument.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import io.opentelemetry.contrib.metrics.micrometer.CallbackRegistration; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.InstrumentState; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +abstract class AbstractInstrument { + private final InstrumentState instrumentState; + private final Logger logger; + private final Tag instrumentationNameTag; + private final Tag instrumentationVersionTag; + private final Map> attributesTagsCache; + + protected AbstractInstrument(InstrumentState instrumentState) { + this.instrumentState = instrumentState; + this.logger = Logger.getLogger(getClass().getName()); + this.instrumentationNameTag = instrumentState.instrumentationScopeNameTag(); + this.instrumentationVersionTag = instrumentState.instrumentationScopeVersionTag(); + this.attributesTagsCache = new ConcurrentHashMap<>(); + } + + protected final MeterRegistry meterRegistry() { + return instrumentState.meterRegistry(); + } + + protected final String name() { + return instrumentState.name(); + } + + @Nullable + protected final String description() { + return instrumentState.description(); + } + + @Nullable + protected final String unit() { + return instrumentState.unit(); + } + + protected final Attributes attributesOrEmpty(@Nullable Attributes attributes) { + return attributes != null ? attributes : Attributes.empty(); + } + + @SuppressWarnings("PreferredInterfaceType") + protected final Iterable attributesToTags(Attributes attributes) { + return attributesTagsCache.computeIfAbsent(attributesOrEmpty(attributes), this::calculateTags); + } + + @SuppressWarnings("PreferredInterfaceType") + private Iterable calculateTags(Attributes attributes) { + List list = new ArrayList<>(attributes.size() + 2); + attributes.forEach( + (attributeKey, value) -> list.add(Tag.of(attributeKey.getKey(), Objects.toString(value)))); + + list.add(instrumentationNameTag); + list.add(instrumentationVersionTag); + return Collections.unmodifiableList(list); + } + + protected final CallbackRegistration registerLongCallback( + Consumer callback, ObservableLongMeasurement measurement) { + return registerCallback(() -> callback.accept(measurement)); + } + + protected final CallbackRegistration registerDoubleCallback( + Consumer callback, ObservableDoubleMeasurement measurement) { + return registerCallback(() -> callback.accept(measurement)); + } + + protected final CallbackRegistration registerCallback(Runnable callback) { + return instrumentState.registerCallback(invokeSafely(callback)); + } + + private Runnable invokeSafely(Runnable runnable) { + return () -> { + try { + runnable.run(); + } catch (Error error) { + throw error; + } catch (Throwable throwable) { + logger.log( + Level.WARNING, + "An exception occurred invoking callback for instrument " + + instrumentState.name() + + ".", + throwable); + } + }; + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractInstrumentBuilder.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractInstrumentBuilder.java new file mode 100644 index 000000000..0c5b21845 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractInstrumentBuilder.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import io.opentelemetry.contrib.metrics.micrometer.internal.state.InstrumentState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import javax.annotation.Nullable; + +abstract class AbstractInstrumentBuilder> { + protected final MeterSharedState meterSharedState; + protected final String name; + @Nullable protected String description; + @Nullable protected String unit; + + protected AbstractInstrumentBuilder(MeterSharedState meterSharedState, String name) { + this.meterSharedState = meterSharedState; + this.name = name; + } + + protected AbstractInstrumentBuilder( + MeterSharedState meterSharedState, + String name, + @Nullable String description, + @Nullable String unit) { + this.meterSharedState = meterSharedState; + this.name = name; + this.description = description; + this.unit = unit; + } + + protected abstract BUILDER self(); + + public BUILDER setDescription(String description) { + this.description = description; + return self(); + } + + public BUILDER setUnit(String unit) { + this.unit = unit; + return self(); + } + + protected InstrumentState createInstrumentState() { + return new InstrumentState(meterSharedState, name, description, unit); + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractUpDownCounter.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractUpDownCounter.java new file mode 100644 index 000000000..2c574d13c --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AbstractUpDownCounter.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import io.micrometer.core.instrument.Gauge; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.InstrumentState; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +abstract class AbstractUpDownCounter extends AbstractInstrument { + private final Map counterMap = new ConcurrentHashMap<>(); + + protected AbstractUpDownCounter(InstrumentState instrumentState) { + super(instrumentState); + } + + protected final void add(Attributes attributes, double value) { + counterMap.computeIfAbsent(attributesOrEmpty(attributes), this::createCounter).increment(value); + } + + protected final void record(double value, Attributes attributes) { + counterMap.computeIfAbsent(attributesOrEmpty(attributes), this::createCounter).set(value); + } + + private AtomicDoubleCounter createCounter(Attributes attributes) { + AtomicDoubleCounter counter = new AtomicDoubleCounter(); + Gauge.builder(name(), counter, AtomicDoubleCounter::current) + .tags(attributesToTags(attributes)) + .description(description()) + .baseUnit(unit()) + .register(meterRegistry()); + return counter; + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AtomicDoubleCounter.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AtomicDoubleCounter.java new file mode 100644 index 000000000..a9e5c02b2 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/AtomicDoubleCounter.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +final class AtomicDoubleCounter { + private static final AtomicLongFieldUpdater BITS_UPDATER = + AtomicLongFieldUpdater.newUpdater(AtomicDoubleCounter.class, "doubleBits"); + + @SuppressWarnings("UnusedVariable") + private volatile long doubleBits; + + double current() { + return Double.longBitsToDouble(doubleBits); + } + + boolean increment(double increment) { + while (true) { + double current = current(); + double update = current + increment; + if (compareAndSet(current, update)) { + return true; + } + } + } + + boolean set(double value) { + BITS_UPDATER.set(this, Double.doubleToRawLongBits(value)); + return true; + } + + boolean setMonotonically(double value) { + while (true) { + double current = current(); + if (current > value) { + return false; + } + if (compareAndSet(current, value)) { + return true; + } + } + } + + boolean compareAndSet(double expected, double update) { + long expectedBits = Double.doubleToRawLongBits(expected); + long updateBits = Double.doubleToRawLongBits(update); + return BITS_UPDATER.compareAndSet(this, expectedBits, updateBits); + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleCounter.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleCounter.java new file mode 100644 index 000000000..87d33e3b6 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleCounter.java @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleCounterBuilder; +import io.opentelemetry.api.metrics.ObservableDoubleCounter; +import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; +import io.opentelemetry.context.Context; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.InstrumentState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import java.util.function.Consumer; +import javax.annotation.Nullable; + +final class MicrometerDoubleCounter extends AbstractCounter implements DoubleCounter { + + private MicrometerDoubleCounter(InstrumentState instrumentState) { + super(instrumentState); + } + + @Override + public void add(double value) { + if (value >= 0.0) { + counter(Attributes.empty()).increment(value); + } + } + + @Override + public void add(double value, Attributes attributes) { + if (value >= 0.0) { + counter(attributes).increment(value); + } + } + + @Override + public void add(double value, Attributes attributes, Context context) { + if (value >= 0.0) { + counter(attributes).increment(value); + } + } + + public static DoubleCounterBuilder builder( + MeterSharedState meterSharedState, + String name, + @Nullable String description, + @Nullable String unit) { + return new Builder(meterSharedState, name, description, unit); + } + + private static class Builder extends AbstractInstrumentBuilder + implements DoubleCounterBuilder { + private Builder( + MeterSharedState meterSharedState, + String name, + @Nullable String description, + @Nullable String unit) { + super(meterSharedState, name, description, unit); + } + + @Override + public Builder self() { + return this; + } + + @Override + public MicrometerDoubleCounter build() { + return new MicrometerDoubleCounter(createInstrumentState()); + } + + @Override + public ObservableDoubleCounter buildWithCallback( + Consumer callback) { + MicrometerDoubleCounter instrument = build(); + return instrument.registerDoubleCallback( + callback, + new ObservableDoubleMeasurement() { + @Override + public void record(double value) { + record(value, Attributes.empty()); + } + + @Override + public void record(double value, Attributes attributes) { + instrument.record(value, attributes); + } + }); + } + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleGauge.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleGauge.java new file mode 100644 index 000000000..1d6a9ba47 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleGauge.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import io.opentelemetry.api.metrics.LongGaugeBuilder; +import io.opentelemetry.api.metrics.ObservableDoubleGauge; +import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.InstrumentState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import java.util.function.Consumer; + +public final class MicrometerDoubleGauge extends AbstractGauge { + + private MicrometerDoubleGauge(InstrumentState instrumentState) { + super(instrumentState); + } + + public static DoubleGaugeBuilder builder(MeterSharedState meterSharedState, String name) { + return new DoubleBuilder(meterSharedState, name); + } + + private static class DoubleBuilder extends AbstractInstrumentBuilder + implements DoubleGaugeBuilder { + + private DoubleBuilder(MeterSharedState meterSharedState, String name) { + super(meterSharedState, name); + } + + @Override + public DoubleBuilder self() { + return this; + } + + @Override + public LongGaugeBuilder ofLongs() { + return MicrometerLongGauge.builder(meterSharedState, name, description, unit); + } + + @Override + public ObservableDoubleGauge buildWithCallback(Consumer callback) { + MicrometerDoubleGauge instrument = new MicrometerDoubleGauge(createInstrumentState()); + return instrument.registerDoubleCallback( + callback, + new ObservableDoubleMeasurement() { + @Override + public void record(double value) { + record(value, Attributes.empty()); + } + + @Override + public void record(double value, Attributes attributes) { + instrument.record(value, attributes); + } + }); + } + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleHistogram.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleHistogram.java new file mode 100644 index 000000000..ca0fcbc8d --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleHistogram.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.LongHistogramBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.InstrumentState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; + +public final class MicrometerDoubleHistogram extends AbstractHistogram implements DoubleHistogram { + + private MicrometerDoubleHistogram(InstrumentState instrumentState) { + super(instrumentState); + } + + @Override + public void record(double value) { + distribution(Attributes.empty()).record(value); + } + + @Override + public void record(double value, Attributes attributes) { + distribution(attributes).record(value); + } + + @Override + public void record(double value, Attributes attributes, Context context) { + distribution(attributes).record(value); + } + + public static DoubleHistogramBuilder builder(MeterSharedState meterSharedState, String name) { + return new Builder(meterSharedState, name); + } + + private static class Builder extends AbstractInstrumentBuilder + implements DoubleHistogramBuilder { + private Builder(MeterSharedState meterSharedState, String name) { + super(meterSharedState, name); + } + + @Override + public Builder self() { + return this; + } + + @Override + public LongHistogramBuilder ofLongs() { + return MicrometerLongHistogram.builder(meterSharedState, name, description, unit); + } + + @Override + public DoubleHistogram build() { + return new MicrometerDoubleHistogram(createInstrumentState()); + } + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleUpDownCounter.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleUpDownCounter.java new file mode 100644 index 000000000..a30f2016c --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleUpDownCounter.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleUpDownCounter; +import io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder; +import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; +import io.opentelemetry.api.metrics.ObservableDoubleUpDownCounter; +import io.opentelemetry.context.Context; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.InstrumentState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import java.util.function.Consumer; +import javax.annotation.Nullable; + +final class MicrometerDoubleUpDownCounter extends AbstractUpDownCounter + implements DoubleUpDownCounter { + private MicrometerDoubleUpDownCounter(InstrumentState instrumentState) { + super(instrumentState); + } + + @Override + public void add(double value) { + add(Attributes.empty(), value); + } + + @Override + public void add(double value, Attributes attributes) { + add(attributes, value); + } + + @Override + public void add(double value, Attributes attributes, Context context) { + add(attributes, value); + } + + static DoubleUpDownCounterBuilder builder( + MeterSharedState meterSharedState, + String name, + @Nullable String description, + @Nullable String unit) { + return new Builder(meterSharedState, name, description, unit); + } + + private static class Builder extends AbstractInstrumentBuilder + implements DoubleUpDownCounterBuilder { + private Builder( + MeterSharedState meterSharedState, + String name, + @Nullable String description, + @Nullable String unit) { + super(meterSharedState, name, description, unit); + } + + @Override + protected Builder self() { + return this; + } + + @Override + public MicrometerDoubleUpDownCounter build() { + return new MicrometerDoubleUpDownCounter(createInstrumentState()); + } + + @Override + public ObservableDoubleUpDownCounter buildWithCallback( + Consumer callback) { + MicrometerDoubleUpDownCounter instrument = build(); + return instrument.registerDoubleCallback( + callback, + new ObservableDoubleMeasurement() { + @Override + public void record(double value) { + instrument.record(value, Attributes.empty()); + } + + @Override + public void record(double value, Attributes attributes) { + instrument.record(value, attributes); + } + }); + } + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongCounter.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongCounter.java new file mode 100644 index 000000000..e973deb79 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongCounter.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleCounterBuilder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.api.metrics.ObservableLongCounter; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import io.opentelemetry.context.Context; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.InstrumentState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import java.util.function.Consumer; + +public final class MicrometerLongCounter extends AbstractCounter implements LongCounter { + + private MicrometerLongCounter(InstrumentState instrumentState) { + super(instrumentState); + } + + @Override + public void add(long value) { + if (value > 0L) { + counter(Attributes.empty()).increment((double) value); + } + } + + @Override + public void add(long value, Attributes attributes) { + if (value > 0L) { + counter(attributes).increment((double) value); + } + } + + @Override + public void add(long value, Attributes attributes, Context context) { + if (value > 0L) { + counter(attributes).increment((double) value); + } + } + + public static LongCounterBuilder builder(MeterSharedState meterSharedState, String name) { + return new Builder(meterSharedState, name); + } + + private static class Builder extends AbstractInstrumentBuilder + implements LongCounterBuilder { + private Builder(MeterSharedState meterSharedState, String name) { + super(meterSharedState, name); + } + + @Override + public Builder self() { + return this; + } + + @Override + public DoubleCounterBuilder ofDoubles() { + return MicrometerDoubleCounter.builder(meterSharedState, name, description, unit); + } + + @Override + public MicrometerLongCounter build() { + return new MicrometerLongCounter(createInstrumentState()); + } + + @Override + public ObservableLongCounter buildWithCallback(Consumer callback) { + MicrometerLongCounter instrument = build(); + return instrument.registerLongCallback( + callback, + new ObservableLongMeasurement() { + @Override + public void record(long value) { + record(value, Attributes.empty()); + } + + @Override + public void record(long value, Attributes attributes) { + instrument.record((double) value, attributes); + } + }); + } + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongGauge.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongGauge.java new file mode 100644 index 000000000..5f9b7cf5b --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongGauge.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongGaugeBuilder; +import io.opentelemetry.api.metrics.ObservableLongGauge; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.InstrumentState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import java.util.function.Consumer; +import javax.annotation.Nullable; + +public final class MicrometerLongGauge extends AbstractGauge { + public MicrometerLongGauge(InstrumentState instrumentState) { + super(instrumentState); + } + + public static LongGaugeBuilder builder( + MeterSharedState meterSharedState, + String name, + @Nullable String description, + @Nullable String unit) { + return new Builder(meterSharedState, name, description, unit); + } + + private static class Builder extends AbstractInstrumentBuilder + implements LongGaugeBuilder { + private Builder( + MeterSharedState meterSharedState, + String name, + @Nullable String description, + @Nullable String unit) { + super(meterSharedState, name, description, unit); + } + + @Override + public Builder self() { + return this; + } + + @Override + public ObservableLongGauge buildWithCallback(Consumer callback) { + MicrometerLongGauge instrument = new MicrometerLongGauge(createInstrumentState()); + return instrument.registerLongCallback( + callback, + new ObservableLongMeasurement() { + @Override + public void record(long value) { + record(value, Attributes.empty()); + } + + @Override + public void record(long value, Attributes attributes) { + instrument.record((double) value, attributes); + } + }); + } + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongHistogram.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongHistogram.java new file mode 100644 index 000000000..4e8e70e82 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongHistogram.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongHistogramBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.InstrumentState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import javax.annotation.Nullable; + +final class MicrometerLongHistogram extends AbstractHistogram implements LongHistogram { + + private MicrometerLongHistogram(InstrumentState instrumentState) { + super(instrumentState); + } + + @Override + public void record(long value) { + distribution(Attributes.empty()).record((double) value); + } + + @Override + public void record(long value, Attributes attributes) { + distribution(attributes).record((double) value); + } + + @Override + public void record(long value, Attributes attributes, Context context) { + distribution(attributes).record((double) value); + } + + public static LongHistogramBuilder builder( + MeterSharedState meterSharedState, + String name, + @Nullable String description, + @Nullable String unit) { + return new Builder(meterSharedState, name, description, unit); + } + + private static class Builder extends AbstractInstrumentBuilder + implements LongHistogramBuilder { + private Builder( + MeterSharedState meterSharedState, + String name, + @Nullable String description, + @Nullable String unit) { + super(meterSharedState, name, description, unit); + } + + @Override + public Builder self() { + return this; + } + + @Override + public LongHistogram build() { + return new MicrometerLongHistogram(createInstrumentState()); + } + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongUpDownCounter.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongUpDownCounter.java new file mode 100644 index 000000000..577881c3a --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongUpDownCounter.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import io.opentelemetry.api.metrics.ObservableLongUpDownCounter; +import io.opentelemetry.context.Context; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.InstrumentState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import java.util.function.Consumer; + +public final class MicrometerLongUpDownCounter extends AbstractUpDownCounter + implements LongUpDownCounter { + private MicrometerLongUpDownCounter(InstrumentState instrumentState) { + super(instrumentState); + } + + @Override + public void add(long value) { + add(Attributes.empty(), (double) value); + } + + @Override + public void add(long value, Attributes attributes) { + add(attributes, (double) value); + } + + @Override + public void add(long value, Attributes attributes, Context context) { + add(attributes, (double) value); + } + + public static LongUpDownCounterBuilder builder(MeterSharedState meterSharedState, String name) { + return new Builder(meterSharedState, name); + } + + private static class Builder extends AbstractInstrumentBuilder + implements LongUpDownCounterBuilder { + private Builder(MeterSharedState meterSharedState, String name) { + super(meterSharedState, name); + } + + @Override + protected Builder self() { + return this; + } + + @Override + public DoubleUpDownCounterBuilder ofDoubles() { + return MicrometerDoubleUpDownCounter.builder(meterSharedState, name, description, unit); + } + + @Override + public MicrometerLongUpDownCounter build() { + return new MicrometerLongUpDownCounter(createInstrumentState()); + } + + @Override + public ObservableLongUpDownCounter buildWithCallback( + Consumer callback) { + MicrometerLongUpDownCounter instrument = build(); + return instrument.registerLongCallback( + callback, + new ObservableLongMeasurement() { + @Override + public void record(long value) { + instrument.record((double) value, Attributes.empty()); + } + + @Override + public void record(long value, Attributes attributes) { + instrument.record((double) value, attributes); + } + }); + } + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/package-info.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/package-info.java new file mode 100644 index 000000000..e950d6120 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/package-info.java @@ -0,0 +1,5 @@ +/** Implementations of OpenTelemetry instruments delegating to Micrometer meters. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/state/InstrumentState.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/state/InstrumentState.java new file mode 100644 index 000000000..73be8f476 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/state/InstrumentState.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.state; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.opentelemetry.contrib.metrics.micrometer.CallbackRegistration; +import javax.annotation.Nullable; + +/** + * State for an instrument. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class InstrumentState { + private final MeterSharedState meterSharedState; + private final String name; + @Nullable private final String description; + @Nullable private final String unit; + + public InstrumentState( + MeterSharedState meterSharedState, + String name, + @Nullable String description, + @Nullable String unit) { + this.meterSharedState = meterSharedState; + this.name = name; + this.description = description; + this.unit = unit; + } + + public MeterRegistry meterRegistry() { + return meterSharedState.meterRegistry(); + } + + public Tag instrumentationScopeNameTag() { + return meterSharedState.instrumentationScopeNameTag(); + } + + public Tag instrumentationScopeVersionTag() { + return meterSharedState.instrumentationScopeVersionTag(); + } + + public CallbackRegistration registerCallback(Runnable runnable) { + return meterSharedState.registerCallback(runnable); + } + + public String name() { + return name; + } + + @Nullable + public String description() { + return description; + } + + @Nullable + public String unit() { + return unit; + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/state/MeterProviderSharedState.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/state/MeterProviderSharedState.java new file mode 100644 index 000000000..f75071abb --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/state/MeterProviderSharedState.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.state; + +import io.micrometer.core.instrument.MeterRegistry; +import io.opentelemetry.contrib.metrics.micrometer.CallbackRegistrar; +import io.opentelemetry.contrib.metrics.micrometer.CallbackRegistration; +import java.util.function.Supplier; + +/** + * State for a meter provider. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class MeterProviderSharedState { + private final Supplier meterRegistrySupplier; + private final CallbackRegistrar callbackRegistrar; + + public MeterProviderSharedState( + Supplier meterRegistrySupplier, CallbackRegistrar callbackRegistrar) { + this.meterRegistrySupplier = meterRegistrySupplier; + this.callbackRegistrar = callbackRegistrar; + } + + public MeterRegistry meterRegistry() { + return meterRegistrySupplier.get(); + } + + public CallbackRegistration registerCallback(Runnable callback) { + return callbackRegistrar.registerCallback(callback); + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/state/MeterSharedState.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/state/MeterSharedState.java new file mode 100644 index 000000000..0e58aef76 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/state/MeterSharedState.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.state; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.util.StringUtils; +import io.opentelemetry.contrib.metrics.micrometer.CallbackRegistration; +import io.opentelemetry.contrib.metrics.micrometer.internal.Constants; +import javax.annotation.Nullable; + +/** + * State for a meter. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class MeterSharedState { + private final MeterProviderSharedState providerSharedState; + private final Tag instrumentationScopeNameTag; + private final Tag instrumentationScopeVersionTag; + @Nullable private final String schemaUrl; + + public MeterSharedState( + MeterProviderSharedState providerSharedState, + String instrumentationScopeName, + @Nullable String instrumentationScopeVersion, + @Nullable String schemaUrl) { + + this.providerSharedState = providerSharedState; + this.instrumentationScopeNameTag = + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, instrumentationScopeName); + if (StringUtils.isNotBlank(instrumentationScopeVersion)) { + this.instrumentationScopeVersionTag = + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, instrumentationScopeVersion); + } else { + this.instrumentationScopeVersionTag = Constants.UNKNOWN_INSTRUMENTATION_VERSION_TAG; + } + this.schemaUrl = schemaUrl; + } + + public MeterRegistry meterRegistry() { + return providerSharedState.meterRegistry(); + } + + public Tag instrumentationScopeNameTag() { + return instrumentationScopeNameTag; + } + + public Tag instrumentationScopeVersionTag() { + return instrumentationScopeVersionTag; + } + + @Nullable + public String schemaUrl() { + return schemaUrl; + } + + public CallbackRegistration registerCallback(Runnable callback) { + return providerSharedState.registerCallback(callback); + } +} diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/state/package-info.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/state/package-info.java new file mode 100644 index 000000000..78c9db226 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/internal/state/package-info.java @@ -0,0 +1,5 @@ +/** Internal state for metrics. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.contrib.metrics.micrometer.internal.state; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/package-info.java b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/package-info.java new file mode 100644 index 000000000..3c3628660 --- /dev/null +++ b/micrometer-meter-provider/src/main/java/io/opentelemetry/contrib/metrics/micrometer/package-info.java @@ -0,0 +1,9 @@ +/** + * A Micrometer implementation of metrics. + * + * @see io.opentelemetry.contrib.metrics.micrometer.MicrometerMeterProvider + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.contrib.metrics.micrometer; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeterProviderTest.java b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeterProviderTest.java new file mode 100644 index 000000000..f323fbaba --- /dev/null +++ b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeterProviderTest.java @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.contrib.metrics.micrometer.internal.Constants; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class MicrometerMeterProviderTest { + SimpleMeterRegistry meterRegistry; + + CallbackRegistrar callbackRegistrar; + + @BeforeEach + void setUp() { + meterRegistry = new SimpleMeterRegistry(); + callbackRegistrar = new TestCallbackRegistrar(Collections.emptyList()); + } + + @Test + void createMeter() { + MeterProvider underTest = + MicrometerMeterProvider.builder(meterRegistry) + .setCallbackRegistrar(callbackRegistrar) + .build(); + Meter meter = underTest.get("name"); + + assertThat(meter) + .isInstanceOfSatisfying( + MicrometerMeter.class, + micrometerMeter -> { + assertThat(micrometerMeter.meterSharedState.instrumentationScopeNameTag()) + .isEqualTo(Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "name")); + assertThat(micrometerMeter.meterSharedState.instrumentationScopeVersionTag()) + .isEqualTo(Constants.UNKNOWN_INSTRUMENTATION_VERSION_TAG); + assertThat(micrometerMeter.meterSharedState.schemaUrl()).isNull(); + }); + } + + @Test + void createMeterWithNameAndVersion() { + MeterProvider underTest = + MicrometerMeterProvider.builder(meterRegistry) + .setCallbackRegistrar(callbackRegistrar) + .build(); + Meter meter = underTest.meterBuilder("name").setInstrumentationVersion("version").build(); + + assertThat(meter) + .isInstanceOfSatisfying( + MicrometerMeter.class, + micrometerMeter -> { + assertThat(micrometerMeter.meterSharedState.instrumentationScopeNameTag()) + .isEqualTo(Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "name")); + assertThat(micrometerMeter.meterSharedState.instrumentationScopeVersionTag()) + .isEqualTo(Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "version")); + assertThat(micrometerMeter.meterSharedState.schemaUrl()).isNull(); + }); + } + + @Test + void createMeterWithNameAndSchemaUrl() { + MeterProvider underTest = + MicrometerMeterProvider.builder(meterRegistry) + .setCallbackRegistrar(callbackRegistrar) + .build(); + Meter meter = underTest.meterBuilder("name").setSchemaUrl("schemaUrl").build(); + + assertThat(meter) + .isInstanceOfSatisfying( + MicrometerMeter.class, + micrometerMeter -> { + assertThat(micrometerMeter.meterSharedState.instrumentationScopeNameTag()) + .isEqualTo(Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "name")); + assertThat(micrometerMeter.meterSharedState.instrumentationScopeVersionTag()) + .isEqualTo(Constants.UNKNOWN_INSTRUMENTATION_VERSION_TAG); + assertThat(micrometerMeter.meterSharedState.schemaUrl()).isEqualTo("schemaUrl"); + }); + } + + @Test + void createMeterWithNameVersionAndSchemaUrl() { + MeterProvider underTest = + MicrometerMeterProvider.builder(meterRegistry) + .setCallbackRegistrar(callbackRegistrar) + .build(); + Meter meter = + underTest + .meterBuilder("name") + .setInstrumentationVersion("version") + .setSchemaUrl("schemaUrl") + .build(); + + assertThat(meter) + .isInstanceOfSatisfying( + MicrometerMeter.class, + micrometerMeter -> { + assertThat(micrometerMeter.meterSharedState.instrumentationScopeNameTag()) + .isEqualTo(Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "name")); + assertThat(micrometerMeter.meterSharedState.instrumentationScopeVersionTag()) + .isEqualTo(Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "version")); + assertThat(micrometerMeter.meterSharedState.schemaUrl()).isEqualTo("schemaUrl"); + }); + } + + @Test + void close() { + CallbackRegistrar registrar = mock(CallbackRegistrar.class); + MicrometerMeterProvider underTest = + MicrometerMeterProvider.builder(meterRegistry).setCallbackRegistrar(registrar).build(); + + underTest.close(); + verify(registrar).close(); + } +} diff --git a/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeterTest.java b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeterTest.java new file mode 100644 index 000000000..56b173e29 --- /dev/null +++ b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/MicrometerMeterTest.java @@ -0,0 +1,120 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.api.metrics.ObservableDoubleGauge; +import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; +import io.opentelemetry.api.metrics.ObservableLongCounter; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import io.opentelemetry.api.metrics.ObservableLongUpDownCounter; +import io.opentelemetry.contrib.metrics.micrometer.internal.instruments.MicrometerDoubleHistogram; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class MicrometerMeterTest { + SimpleMeterRegistry meterRegistry; + + List callbacks; + + TestCallbackRegistrar callbackRegistrar; + + Meter underTest; + + @Mock Consumer longMeasurementConsumer; + + @Mock Consumer doubleMeasurementConsumer; + + @BeforeEach + void setUp() { + meterRegistry = new SimpleMeterRegistry(); + callbacks = new ArrayList<>(); + callbackRegistrar = new TestCallbackRegistrar(callbacks); + MeterProvider meterProvider = + new MicrometerMeterProvider(() -> meterRegistry, callbackRegistrar); + underTest = meterProvider.get("meter"); + } + + @Test + void observesCounterOnPoll() { + ObservableLongCounter counter = + underTest.counterBuilder("counter").buildWithCallback(longMeasurementConsumer); + + assertThat(callbacks).isNotEmpty(); + verifyNoInteractions(longMeasurementConsumer); + + callbackRegistrar.run(); + verify(longMeasurementConsumer).accept(any()); + callbackRegistrar.run(); + verify(longMeasurementConsumer, times(2)).accept(any()); + callbackRegistrar.run(); + verify(longMeasurementConsumer, times(3)).accept(any()); + + counter.close(); + assertThat(callbacks).isEmpty(); + } + + @Test + void observesUpDownCounterOnPoll() { + ObservableLongUpDownCounter counter = + underTest.upDownCounterBuilder("upDownCounter").buildWithCallback(longMeasurementConsumer); + + assertThat(callbacks).isNotEmpty(); + verifyNoInteractions(longMeasurementConsumer); + + callbackRegistrar.run(); + verify(longMeasurementConsumer).accept(any()); + callbackRegistrar.run(); + verify(longMeasurementConsumer, times(2)).accept(any()); + callbackRegistrar.run(); + verify(longMeasurementConsumer, times(3)).accept(any()); + + counter.close(); + assertThat(callbacks).isEmpty(); + } + + @Test + void observesGaugeOnPoll() { + ObservableDoubleGauge counter = + underTest.gaugeBuilder("gauge").buildWithCallback(doubleMeasurementConsumer); + + assertThat(callbacks).isNotEmpty(); + verifyNoInteractions(doubleMeasurementConsumer); + + callbackRegistrar.run(); + verify(doubleMeasurementConsumer, times(1)).accept(any()); + callbackRegistrar.run(); + verify(doubleMeasurementConsumer, times(2)).accept(any()); + callbackRegistrar.run(); + verify(doubleMeasurementConsumer, times(3)).accept(any()); + + counter.close(); + assertThat(callbacks).isEmpty(); + } + + @Test + void createsHistogramMeter() { + DoubleHistogram histogram = underTest.histogramBuilder("histogram").build(); + + assertThat(histogram).isInstanceOf(MicrometerDoubleHistogram.class); + } +} diff --git a/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/ScheduledCallbackRegistrarTest.java b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/ScheduledCallbackRegistrarTest.java new file mode 100644 index 000000000..f87794ea7 --- /dev/null +++ b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/ScheduledCallbackRegistrarTest.java @@ -0,0 +1,166 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import java.time.Duration; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ScheduledCallbackRegistrarTest { + + @Mock ScheduledExecutorService scheduledExecutorService; + + @Mock Runnable callback; + + @Mock ScheduledFuture scheduledFuture; + + @Captor ArgumentCaptor pollingRunnableCaptor; + + @Test + void schedulesCallback() { + doReturn(scheduledFuture) + .when(scheduledExecutorService) + .scheduleAtFixedRate(any(), anyLong(), anyLong(), any()); + + try (CallbackRegistrar underTest = + ScheduledCallbackRegistrar.builder(scheduledExecutorService).build()) { + + verifyNoInteractions(scheduledExecutorService); + + underTest.registerCallback(callback); + + verify(scheduledExecutorService) + .scheduleAtFixedRate( + pollingRunnableCaptor.capture(), eq(1L), eq(1L), eq(TimeUnit.SECONDS)); + Runnable pollingRunnable = pollingRunnableCaptor.getValue(); + + verifyNoInteractions(callback); + + pollingRunnable.run(); + verify(callback).run(); + } + } + + @Test + @SuppressWarnings("PreferJavaTimeOverload") + void schedulesCallbackWithPeriod() { + doReturn(scheduledFuture) + .when(scheduledExecutorService) + .scheduleAtFixedRate(any(), anyLong(), anyLong(), any()); + + try (CallbackRegistrar underTest = + ScheduledCallbackRegistrar.builder(scheduledExecutorService) + .setPeriod(10L, TimeUnit.SECONDS) + .build()) { + + verifyNoInteractions(scheduledExecutorService); + + underTest.registerCallback(callback); + + verify(scheduledExecutorService) + .scheduleAtFixedRate( + pollingRunnableCaptor.capture(), eq(10L), eq(10L), eq(TimeUnit.SECONDS)); + Runnable pollingRunnable = pollingRunnableCaptor.getValue(); + + verifyNoInteractions(callback); + + pollingRunnable.run(); + verify(callback).run(); + } + } + + @Test + void schedulesCallbackWithPeriodDuration() { + doReturn(scheduledFuture) + .when(scheduledExecutorService) + .scheduleAtFixedRate(any(), anyLong(), anyLong(), any()); + + try (CallbackRegistrar underTest = + ScheduledCallbackRegistrar.builder(scheduledExecutorService) + .setPeriod(Duration.ofSeconds(10L)) + .build()) { + + verifyNoInteractions(scheduledExecutorService); + + underTest.registerCallback(callback); + + verify(scheduledExecutorService) + .scheduleAtFixedRate( + pollingRunnableCaptor.capture(), eq(10_000L), eq(10_000L), eq(TimeUnit.MILLISECONDS)); + Runnable pollingRunnable = pollingRunnableCaptor.getValue(); + + verifyNoInteractions(callback); + + pollingRunnable.run(); + verify(callback).run(); + } + } + + @Test + void handlesNullCallback() { + try (CallbackRegistrar underTest = + ScheduledCallbackRegistrar.builder(scheduledExecutorService).build()) { + + underTest.registerCallback(null); + + verifyNoInteractions(scheduledExecutorService); + } + } + + @Test + void closeCancelsScheduledFuture() { + doReturn(scheduledFuture) + .when(scheduledExecutorService) + .scheduleAtFixedRate(any(), anyLong(), anyLong(), any()); + + try (CallbackRegistrar underTest = + ScheduledCallbackRegistrar.builder(scheduledExecutorService).build()) { + + underTest.registerCallback(callback); + verify(scheduledExecutorService) + .scheduleAtFixedRate(any(), eq(1L), eq(1L), eq(TimeUnit.SECONDS)); + } + verify(scheduledFuture).cancel(false); + } + + @Test + void closeShutsDownScheduledExecutorService() { + CallbackRegistrar underTest = + ScheduledCallbackRegistrar.builder(scheduledExecutorService) + .setShutdownExecutorOnClose(true) + .build(); + + underTest.close(); + verify(scheduledExecutorService).shutdown(); + } + + @Test + void closeDoesNotShutDownScheduledExecutorService() { + CallbackRegistrar underTest = + ScheduledCallbackRegistrar.builder(scheduledExecutorService) + .setShutdownExecutorOnClose(false) + .build(); + + underTest.close(); + verify(scheduledExecutorService, never()).shutdown(); + } +} diff --git a/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/TestCallbackRegistrar.java b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/TestCallbackRegistrar.java new file mode 100644 index 000000000..2be705618 --- /dev/null +++ b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/TestCallbackRegistrar.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer; + +import java.util.List; + +public class TestCallbackRegistrar implements CallbackRegistrar, Runnable { + + private final List callbacks; + + public TestCallbackRegistrar(List callbacks) { + this.callbacks = callbacks; + } + + @Override + public CallbackRegistration registerCallback(Runnable callback) { + callbacks.add(callback); + return () -> callbacks.remove(callback); + } + + @Override + public void close() { + callbacks.clear(); + } + + @Override + public void run() { + for (Runnable callback : callbacks) { + callback.run(); + } + } +} diff --git a/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/MemoizingSupplierTest.java b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/MemoizingSupplierTest.java new file mode 100644 index 000000000..d39405c20 --- /dev/null +++ b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/MemoizingSupplierTest.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.util.function.Supplier; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class MemoizingSupplierTest { + + @Mock Supplier supplier; + + @Test + void callsGetOnlyOnce() { + when(supplier.get()).thenReturn("RESULT"); + + Supplier underTest = new MemoizingSupplier<>(supplier); + + verifyNoInteractions(supplier); + + assertThat(underTest.get()).isEqualTo("RESULT"); + verify(supplier, times(1)).get(); + + assertThat(underTest.get()).isEqualTo("RESULT"); + verifyNoMoreInteractions(supplier); + } +} diff --git a/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/PollingMeterCallbackRegistrarTest.java b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/PollingMeterCallbackRegistrarTest.java new file mode 100644 index 000000000..92c582718 --- /dev/null +++ b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/PollingMeterCallbackRegistrarTest.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.opentelemetry.contrib.metrics.micrometer.CallbackRegistration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PollingMeterCallbackRegistrarTest { + + SimpleMeterRegistry meterRegistry; + + @BeforeEach + void setUp() { + meterRegistry = new SimpleMeterRegistry(); + } + + @Test + void createsPollingMeterOnCallbackRegistration() { + Meter pollingMeter; + try (PollingMeterCallbackRegistrar underTest = + new PollingMeterCallbackRegistrar(() -> meterRegistry)) { + + pollingMeter = meterRegistry.find("otel_polling_meter").meter(); + assertThat(pollingMeter).isNull(); + + underTest.registerCallback(() -> {}); + + pollingMeter = meterRegistry.find("otel_polling_meter").meter(); + assertThat(pollingMeter).isNotNull(); + } + pollingMeter = meterRegistry.find("otel_polling_meter").meter(); + assertThat(pollingMeter).isNull(); + } + + @Test + void pollingMeterInvokesCallback() { + Meter pollingMeter; + try (PollingMeterCallbackRegistrar underTest = + new PollingMeterCallbackRegistrar(() -> meterRegistry)) { + + pollingMeter = meterRegistry.find("otel_polling_meter").meter(); + assertThat(pollingMeter).isNull(); + + Runnable callback = mock(Runnable.class); + try (CallbackRegistration registration = underTest.registerCallback(callback)) { + + pollingMeter = meterRegistry.find("otel_polling_meter").meter(); + assertThat(pollingMeter).isNotNull(); + + verifyNoInteractions(callback); + + pollingMeter.measure().forEach(measurement -> {}); + verify(callback).run(); + } + + pollingMeter.measure().forEach(measurement -> {}); + verifyNoMoreInteractions(callback); + } + } +} diff --git a/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleCounterTest.java b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleCounterTest.java new file mode 100644 index 000000000..4e49e9767 --- /dev/null +++ b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleCounterTest.java @@ -0,0 +1,283 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.ObservableDoubleCounter; +import io.opentelemetry.context.Context; +import io.opentelemetry.contrib.metrics.micrometer.TestCallbackRegistrar; +import io.opentelemetry.contrib.metrics.micrometer.internal.Constants; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterProviderSharedState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class MicrometerDoubleCounterTest { + + SimpleMeterRegistry meterRegistry; + + List callbacks; + + TestCallbackRegistrar callbackRegistrar; + + MeterProviderSharedState meterProviderSharedState; + + MeterSharedState meterSharedState; + + @BeforeEach + void setUp() { + meterRegistry = new SimpleMeterRegistry(); + callbacks = new ArrayList<>(); + callbackRegistrar = new TestCallbackRegistrar(callbacks); + meterProviderSharedState = new MeterProviderSharedState(() -> meterRegistry, callbackRegistrar); + meterSharedState = new MeterSharedState(meterProviderSharedState, "meter", "1.0", null); + } + + @Test + void add() { + DoubleCounter underTest = + MicrometerLongCounter.builder(meterSharedState, "counter") + .ofDoubles() + .setDescription("description") + .setUnit("unit") + .build(); + + underTest.add(10.0); + + Counter counter = meterRegistry.find("counter").counter(); + assertThat(counter).isNotNull(); + Meter.Id id = counter.getId(); + assertThat(id.getName()).isEqualTo("counter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(counter.count()).isEqualTo(10.0); + + // test that counter can be increased + underTest.add(10.0); + assertThat(counter.count()).isEqualTo(20.0); + + // test that counter cannot be decreased + underTest.add(-5.0); + assertThat(counter.count()).isEqualTo(20.0); + + double expectedCount = 20.0; + for (double value : RandomUtils.randomDoubles(10, 0.0, 10.0)) { + expectedCount += value; + + underTest.add(value); + assertThat(counter.count()).isEqualTo(expectedCount); + } + } + + @Test + void addWithAttributes() { + DoubleCounter underTest = + MicrometerLongCounter.builder(meterSharedState, "counter") + .ofDoubles() + .setDescription("description") + .setUnit("unit") + .build(); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + Attributes attributes = Attributes.builder().put("key", "value").build(); + underTest.add(10.0, attributes); + + Counter counter = meterRegistry.find("counter").counter(); + assertThat(counter).isNotNull(); + Meter.Id id = counter.getId(); + assertThat(id.getName()).isEqualTo("counter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(counter.count()).isEqualTo(10.0); + + // test that counter can be increased + underTest.add(10.0, attributes); + assertThat(counter.count()).isEqualTo(20.0); + + // test that counter cannot be decreased + underTest.add(-5.0, attributes); + assertThat(counter.count()).isEqualTo(20.0); + + double expectedCount = 20.0; + for (double value : RandomUtils.randomDoubles(10, 0.0, 10.0)) { + expectedCount += value; + + underTest.add(value, attributes); + assertThat(counter.count()).isEqualTo(expectedCount); + } + } + + @Test + void addWithAttributesAndContext() { + DoubleCounter underTest = + MicrometerLongCounter.builder(meterSharedState, "counter") + .ofDoubles() + .setDescription("description") + .setUnit("unit") + .build(); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + Attributes attributes = Attributes.builder().put("key", "value").build(); + underTest.add(10.0, attributes, Context.root()); + + Counter counter = meterRegistry.find("counter").counter(); + assertThat(counter).isNotNull(); + Meter.Id id = counter.getId(); + assertThat(id.getName()).isEqualTo("counter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(counter.count()).isEqualTo(10.0); + + // test that counter can be increased + underTest.add(10.0, attributes, Context.root()); + assertThat(counter.count()).isEqualTo(20.0); + + // test that counter cannot be decreased + underTest.add(-5.0, attributes, Context.root()); + assertThat(counter.count()).isEqualTo(20.0); + + double expectedCount = 20.0; + for (double value : RandomUtils.randomDoubles(10, 0.0, 10.0)) { + expectedCount += value; + + underTest.add(value, attributes, Context.root()); + assertThat(counter.count()).isEqualTo(expectedCount); + } + } + + @Test + void observable() { + AtomicDoubleCounter atomicDoubleCounter = new AtomicDoubleCounter(); + ObservableDoubleCounter underTest = + MicrometerLongCounter.builder(meterSharedState, "counter") + .ofDoubles() + .setDescription("description") + .setUnit("unit") + .buildWithCallback(measurement -> measurement.record(atomicDoubleCounter.current())); + + assertThat(callbacks).hasSize(1); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + atomicDoubleCounter.set(10.0); + callbackRegistrar.run(); + FunctionCounter counter = meterRegistry.find("counter").functionCounter(); + assertThat(counter).isNotNull(); + Meter.Id id = counter.getId(); + assertThat(id.getName()).isEqualTo("counter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(counter.count()).isEqualTo(10.0); + + // test that counter can be increased + atomicDoubleCounter.set(20.0); + callbackRegistrar.run(); + assertThat(counter.count()).isEqualTo(20.0); + + // test that counter cannot be decreased + atomicDoubleCounter.set(5.0); + callbackRegistrar.run(); + assertThat(counter.count()).isEqualTo(20.0); + + double expectedCount = 20.0; + for (double value : RandomUtils.randomDoubles(10, 0.0, 500.0)) { + expectedCount += value; + + atomicDoubleCounter.set(expectedCount); + callbackRegistrar.run(); + assertThat(counter.count()).isEqualTo(expectedCount); + } + + underTest.close(); + + assertThat(callbacks).isEmpty(); + } + + @Test + void observableWithAttributes() { + AtomicDoubleCounter atomicDoubleCounter = new AtomicDoubleCounter(); + Attributes attributes = Attributes.builder().put("key", "value").build(); + ObservableDoubleCounter underTest = + MicrometerLongCounter.builder(meterSharedState, "counter") + .ofDoubles() + .setDescription("description") + .setUnit("unit") + .buildWithCallback( + measurement -> measurement.record(atomicDoubleCounter.current(), attributes)); + + assertThat(callbacks).hasSize(1); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + atomicDoubleCounter.set(10.0); + callbackRegistrar.run(); + FunctionCounter counter = meterRegistry.find("counter").functionCounter(); + assertThat(counter).isNotNull(); + Meter.Id id = counter.getId(); + assertThat(id.getName()).isEqualTo("counter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(counter.count()).isEqualTo(10.0); + + // test that counter can be increased + atomicDoubleCounter.set(20.0); + callbackRegistrar.run(); + assertThat(counter.count()).isEqualTo(20.0); + + // test that counter cannot be decreased + atomicDoubleCounter.set(5.0); + callbackRegistrar.run(); + assertThat(counter.count()).isEqualTo(20.0); + + double expectedCount = 10.0; + for (double value : RandomUtils.randomDoubles(10, 0.0, 500.0)) { + expectedCount += value; + + atomicDoubleCounter.set(expectedCount); + callbackRegistrar.run(); + assertThat(counter.count()).isEqualTo(expectedCount); + } + + underTest.close(); + + assertThat(callbacks).isEmpty(); + } +} diff --git a/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleGaugeTest.java b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleGaugeTest.java new file mode 100644 index 000000000..18d0cc8aa --- /dev/null +++ b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleGaugeTest.java @@ -0,0 +1,120 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.ObservableDoubleGauge; +import io.opentelemetry.contrib.metrics.micrometer.TestCallbackRegistrar; +import io.opentelemetry.contrib.metrics.micrometer.internal.Constants; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterProviderSharedState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class MicrometerDoubleGaugeTest { + SimpleMeterRegistry meterRegistry; + + List callbacks; + + TestCallbackRegistrar callbackRegistrar; + + MeterProviderSharedState meterProviderSharedState; + + MeterSharedState meterSharedState; + + @BeforeEach + void setUp() { + meterRegistry = new SimpleMeterRegistry(); + callbacks = new ArrayList<>(); + callbackRegistrar = new TestCallbackRegistrar(callbacks); + meterProviderSharedState = new MeterProviderSharedState(() -> meterRegistry, callbackRegistrar); + meterSharedState = new MeterSharedState(meterProviderSharedState, "meter", "1.0", null); + } + + @Test + void observable() { + AtomicDoubleCounter atomicDoubleCounter = new AtomicDoubleCounter(); + + ObservableDoubleGauge underTest = + MicrometerDoubleGauge.builder(meterSharedState, "gauge") + .setDescription("description") + .setUnit("unit") + .buildWithCallback(measurement -> measurement.record(atomicDoubleCounter.current())); + + assertThat(callbacks).hasSize(1); + + atomicDoubleCounter.set(10.0); + callbackRegistrar.run(); + Gauge gauge = meterRegistry.find("gauge").gauge(); + assertThat(gauge).isNotNull(); + Meter.Id id = gauge.getId(); + assertThat(id.getName()).isEqualTo("gauge"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(gauge.value()).isEqualTo(10.0); + + for (double value : RandomUtils.randomDoubles(10, 0.0, 500.0)) { + atomicDoubleCounter.set(value); + callbackRegistrar.run(); + assertThat(gauge.value()).isEqualTo(value); + } + + underTest.close(); + + assertThat(callbacks).isEmpty(); + } + + @Test + void observableWithAttributes() { + AtomicDoubleCounter atomicDoubleCounter = new AtomicDoubleCounter(); + Attributes attributes = Attributes.builder().put("key", "value").build(); + ObservableDoubleGauge underTest = + MicrometerDoubleGauge.builder(meterSharedState, "gauge") + .setDescription("description") + .setUnit("unit") + .buildWithCallback( + measurement -> measurement.record(atomicDoubleCounter.current(), attributes)); + + assertThat(callbacks).hasSize(1); + + atomicDoubleCounter.set(10.0); + callbackRegistrar.run(); + Gauge gauge = meterRegistry.find("gauge").gauge(); + assertThat(gauge).isNotNull(); + Meter.Id id = gauge.getId(); + assertThat(id.getName()).isEqualTo("gauge"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(gauge.value()).isEqualTo(10.0); + + for (double value : RandomUtils.randomDoubles(10, 0.0, 500.0)) { + atomicDoubleCounter.set(value); + callbackRegistrar.run(); + assertThat(gauge.value()).isEqualTo(value); + } + + underTest.close(); + + assertThat(callbacks).isEmpty(); + } +} diff --git a/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleHistogramTest.java b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleHistogramTest.java new file mode 100644 index 000000000..545bd77d6 --- /dev/null +++ b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleHistogramTest.java @@ -0,0 +1,155 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.context.Context; +import io.opentelemetry.contrib.metrics.micrometer.TestCallbackRegistrar; +import io.opentelemetry.contrib.metrics.micrometer.internal.Constants; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterProviderSharedState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class MicrometerDoubleHistogramTest { + + SimpleMeterRegistry meterRegistry; + + TestCallbackRegistrar callbackRegistrar; + + MeterProviderSharedState meterProviderSharedState; + + MeterSharedState meterSharedState; + + @BeforeEach + void setUp() { + meterRegistry = new SimpleMeterRegistry(); + callbackRegistrar = new TestCallbackRegistrar(Collections.emptyList()); + meterProviderSharedState = new MeterProviderSharedState(() -> meterRegistry, callbackRegistrar); + meterSharedState = new MeterSharedState(meterProviderSharedState, "meter", "1.0", null); + } + + @Test + void add() { + DoubleHistogram underTest = + MicrometerDoubleHistogram.builder(meterSharedState, "histogram") + .setDescription("description") + .setUnit("unit") + .build(); + + underTest.record(10.0); + + DistributionSummary summary = meterRegistry.find("histogram").summary(); + assertThat(summary).isNotNull(); + Meter.Id id = summary.getId(); + assertThat(id.getName()).isEqualTo("histogram"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(summary.count()).isEqualTo(1); + assertThat(summary.totalAmount()).isEqualTo(10.0); + + long expectedCount = 1; + double expectedTotal = 10.0; + for (double value : RandomUtils.randomDoubles(10, 0.0, 10.0)) { + expectedCount += 1; + expectedTotal += value; + + underTest.record(value); + assertThat(summary.count()).isEqualTo(expectedCount); + assertThat(summary.totalAmount()).isEqualTo(expectedTotal); + } + } + + @Test + void addWithAttributes() { + DoubleHistogram underTest = + MicrometerDoubleHistogram.builder(meterSharedState, "histogram") + .setDescription("description") + .setUnit("unit") + .build(); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + Attributes attributes = Attributes.builder().put("key", "value").build(); + underTest.record(10.0, attributes); + + DistributionSummary summary = meterRegistry.find("histogram").summary(); + assertThat(summary).isNotNull(); + Meter.Id id = summary.getId(); + assertThat(id.getName()).isEqualTo("histogram"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(summary.count()).isEqualTo(1); + assertThat(summary.totalAmount()).isEqualTo(10.0); + + long expectedCount = 1; + double expectedTotal = 10.0; + for (double value : RandomUtils.randomDoubles(10, 0.0, 10.0)) { + expectedCount += 1; + expectedTotal += value; + + underTest.record(value, attributes); + assertThat(summary.count()).isEqualTo(expectedCount); + assertThat(summary.totalAmount()).isEqualTo(expectedTotal); + } + } + + @Test + void addWithAttributesAndContext() { + DoubleHistogram underTest = + MicrometerDoubleHistogram.builder(meterSharedState, "histogram") + .setDescription("description") + .setUnit("unit") + .build(); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + Attributes attributes = Attributes.builder().put("key", "value").build(); + underTest.record(10.0, attributes, Context.root()); + + DistributionSummary summary = meterRegistry.find("histogram").summary(); + assertThat(summary).isNotNull(); + Meter.Id id = summary.getId(); + assertThat(id.getName()).isEqualTo("histogram"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(summary.count()).isEqualTo(1); + assertThat(summary.totalAmount()).isEqualTo(10.0); + + long expectedCount = 1; + double expectedTotal = 10.0; + for (double value : RandomUtils.randomDoubles(10, 0.0, 10.0)) { + expectedCount += 1; + expectedTotal += value; + + underTest.record(value, attributes, Context.root()); + assertThat(summary.count()).isEqualTo(expectedCount); + assertThat(summary.totalAmount()).isEqualTo(expectedTotal); + } + } +} diff --git a/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleUpDownCounterTest.java b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleUpDownCounterTest.java new file mode 100644 index 000000000..bef6cbbb9 --- /dev/null +++ b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerDoubleUpDownCounterTest.java @@ -0,0 +1,249 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleUpDownCounter; +import io.opentelemetry.api.metrics.ObservableDoubleUpDownCounter; +import io.opentelemetry.context.Context; +import io.opentelemetry.contrib.metrics.micrometer.TestCallbackRegistrar; +import io.opentelemetry.contrib.metrics.micrometer.internal.Constants; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterProviderSharedState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class MicrometerDoubleUpDownCounterTest { + + SimpleMeterRegistry meterRegistry; + + List callbacks; + + TestCallbackRegistrar callbackRegistrar; + + MeterProviderSharedState meterProviderSharedState; + + MeterSharedState meterSharedState; + + @BeforeEach + void setUp() { + meterRegistry = new SimpleMeterRegistry(); + callbacks = new ArrayList<>(); + callbackRegistrar = new TestCallbackRegistrar(callbacks); + meterProviderSharedState = new MeterProviderSharedState(() -> meterRegistry, callbackRegistrar); + meterSharedState = new MeterSharedState(meterProviderSharedState, "meter", "1.0", null); + } + + @Test + void add() { + DoubleUpDownCounter underTest = + MicrometerLongUpDownCounter.builder(meterSharedState, "upDownCounter") + .ofDoubles() + .setDescription("description") + .setUnit("unit") + .build(); + + underTest.add(10.0); + + Gauge gauge = meterRegistry.find("upDownCounter").gauge(); + assertThat(gauge).isNotNull(); + Meter.Id id = gauge.getId(); + assertThat(id.getName()).isEqualTo("upDownCounter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(gauge.value()).isEqualTo(10.0); + + underTest.add(-10.0); + assertThat(gauge.value()).isEqualTo(0.0); + + double expectedCount = 0.0; + for (double value : RandomUtils.randomDoubles(10, -500.0, 500.0)) { + expectedCount += value; + + underTest.add(value); + assertThat(gauge.value()).isEqualTo(expectedCount); + } + } + + @Test + void addWithAttributes() { + DoubleUpDownCounter underTest = + MicrometerLongUpDownCounter.builder(meterSharedState, "upDownCounter") + .ofDoubles() + .setDescription("description") + .setUnit("unit") + .build(); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + Attributes attributes = Attributes.builder().put("key", "value").build(); + underTest.add(10.0, attributes); + + Gauge gauge = meterRegistry.find("upDownCounter").gauge(); + assertThat(gauge).isNotNull(); + Meter.Id id = gauge.getId(); + assertThat(id.getName()).isEqualTo("upDownCounter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(gauge.value()).isEqualTo(10.0); + + underTest.add(-10.0, attributes); + assertThat(gauge.value()).isEqualTo(0.0); + + double expectedCount = 0.0; + for (double value : RandomUtils.randomDoubles(10, -500.0, 500.0)) { + expectedCount += value; + + underTest.add(value, attributes); + assertThat(gauge.value()).isEqualTo(expectedCount); + } + } + + @Test + void addWithAttributesAndContext() { + DoubleUpDownCounter underTest = + MicrometerLongUpDownCounter.builder(meterSharedState, "upDownCounter") + .ofDoubles() + .setDescription("description") + .setUnit("unit") + .build(); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + Attributes attributes = Attributes.builder().put("key", "value").build(); + underTest.add(10.0, attributes, Context.root()); + + Gauge gauge = meterRegistry.find("upDownCounter").gauge(); + assertThat(gauge).isNotNull(); + Meter.Id id = gauge.getId(); + assertThat(id.getName()).isEqualTo("upDownCounter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(gauge.value()).isEqualTo(10.0); + + underTest.add(-10, attributes, Context.root()); + assertThat(gauge.value()).isEqualTo(0.0); + + double expectedCount = 0.0; + for (double value : RandomUtils.randomDoubles(10, -500.0, 500.0)) { + expectedCount += value; + + underTest.add(value, attributes, Context.root()); + assertThat(gauge.value()).isEqualTo(expectedCount); + } + } + + @Test + void observable() { + AtomicDoubleCounter atomicDoubleCounter = new AtomicDoubleCounter(); + ObservableDoubleUpDownCounter underTest = + MicrometerLongUpDownCounter.builder(meterSharedState, "upDownCounter") + .ofDoubles() + .setDescription("description") + .setUnit("unit") + .buildWithCallback(measurement -> measurement.record(atomicDoubleCounter.current())); + + assertThat(callbacks).hasSize(1); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + atomicDoubleCounter.set(10.0); + callbackRegistrar.run(); + Gauge gauge = meterRegistry.find("upDownCounter").gauge(); + assertThat(gauge).isNotNull(); + Meter.Id id = gauge.getId(); + assertThat(id.getName()).isEqualTo("upDownCounter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(gauge.value()).isEqualTo(10.0); + + atomicDoubleCounter.set(0.0); + callbackRegistrar.run(); + assertThat(gauge.value()).isEqualTo(0.0); + + for (double value : RandomUtils.randomDoubles(10, -500.0, 500.0)) { + atomicDoubleCounter.set(value); + callbackRegistrar.run(); + assertThat(gauge.value()).isEqualTo(value); + } + + underTest.close(); + + assertThat(callbacks).isEmpty(); + } + + @Test + void observableWithAttributes() { + AtomicDoubleCounter atomicDoubleCounter = new AtomicDoubleCounter(); + Attributes attributes = Attributes.builder().put("key", "value").build(); + ObservableDoubleUpDownCounter underTest = + MicrometerLongUpDownCounter.builder(meterSharedState, "upDownCounter") + .ofDoubles() + .setDescription("description") + .setUnit("unit") + .buildWithCallback( + measurement -> measurement.record(atomicDoubleCounter.current(), attributes)); + + assertThat(callbacks).hasSize(1); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + atomicDoubleCounter.set(10.0); + callbackRegistrar.run(); + Gauge gauge = meterRegistry.find("upDownCounter").gauge(); + assertThat(gauge).isNotNull(); + Meter.Id id = gauge.getId(); + assertThat(id.getName()).isEqualTo("upDownCounter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(gauge.value()).isEqualTo(10.0); + + atomicDoubleCounter.set(0.0); + callbackRegistrar.run(); + assertThat(gauge.value()).isEqualTo(0.0); + + for (double value : RandomUtils.randomDoubles(10, -500.0, 500.0)) { + atomicDoubleCounter.set(value); + callbackRegistrar.run(); + assertThat(gauge.value()).isEqualTo(value); + } + + underTest.close(); + + assertThat(callbacks).isEmpty(); + } +} diff --git a/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongCounterTest.java b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongCounterTest.java new file mode 100644 index 000000000..ca4aa6e8f --- /dev/null +++ b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongCounterTest.java @@ -0,0 +1,280 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.ObservableLongCounter; +import io.opentelemetry.context.Context; +import io.opentelemetry.contrib.metrics.micrometer.TestCallbackRegistrar; +import io.opentelemetry.contrib.metrics.micrometer.internal.Constants; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterProviderSharedState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class MicrometerLongCounterTest { + + SimpleMeterRegistry meterRegistry; + + List callbacks; + + TestCallbackRegistrar callbackRegistrar; + + MeterProviderSharedState meterProviderSharedState; + + MeterSharedState meterSharedState; + + @BeforeEach + void setUp() { + meterRegistry = new SimpleMeterRegistry(); + callbacks = new ArrayList<>(); + callbackRegistrar = new TestCallbackRegistrar(callbacks); + meterProviderSharedState = new MeterProviderSharedState(() -> meterRegistry, callbackRegistrar); + meterSharedState = new MeterSharedState(meterProviderSharedState, "meter", "1.0", null); + } + + @Test + void add() { + LongCounter underTest = + MicrometerLongCounter.builder(meterSharedState, "counter") + .setDescription("description") + .setUnit("unit") + .build(); + + underTest.add(10); + + Counter counter = meterRegistry.find("counter").counter(); + assertThat(counter).isNotNull(); + Meter.Id id = counter.getId(); + assertThat(id.getName()).isEqualTo("counter"); + assertThat(id.getTags()) + .isEqualTo( + Arrays.asList( + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0"))); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(counter.count()).isEqualTo(10.0); + + // test that counter can be increased + underTest.add(10); + assertThat(counter.count()).isEqualTo(20.0); + + // test that counter cannot be decreased + underTest.add(-5); + assertThat(counter.count()).isEqualTo(20.0); + + double expectedCount = 20.0; + for (long value : RandomUtils.randomLongs(10, 0L, 500L)) { + expectedCount += value; + + underTest.add(value); + assertThat(counter.count()).isEqualTo(expectedCount); + } + } + + @Test + void addWithAttributes() { + LongCounter underTest = + MicrometerLongCounter.builder(meterSharedState, "counter") + .setDescription("description") + .setUnit("unit") + .build(); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + Attributes attributes = Attributes.builder().put("key", "value").build(); + underTest.add(10, attributes); + + Counter counter = meterRegistry.find("counter").counter(); + assertThat(counter).isNotNull(); + Meter.Id id = counter.getId(); + assertThat(id.getName()).isEqualTo("counter"); + assertThat(id.getTags()) + .isEqualTo( + Arrays.asList( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0"))); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(counter.count()).isEqualTo(10.0); + + // test that counter can be increased + underTest.add(10, attributes); + assertThat(counter.count()).isEqualTo(20.0); + + // test that counter cannot be decreased + underTest.add(-5, attributes); + assertThat(counter.count()).isEqualTo(20.0); + + double expectedCount = 20.0; + for (long value : RandomUtils.randomLongs(10, 0L, 500L)) { + expectedCount += value; + + underTest.add(value, attributes); + assertThat(counter.count()).isEqualTo(expectedCount); + } + } + + @Test + void addWithAttributesAndContext() { + LongCounter underTest = + MicrometerLongCounter.builder(meterSharedState, "counter") + .setDescription("description") + .setUnit("unit") + .build(); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + Attributes attributes = Attributes.builder().put("key", "value").build(); + underTest.add(10, attributes, Context.root()); + + Counter counter = meterRegistry.find("counter").counter(); + assertThat(counter).isNotNull(); + Meter.Id id = counter.getId(); + assertThat(id.getName()).isEqualTo("counter"); + assertThat(id.getTags()) + .isEqualTo( + Arrays.asList( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0"))); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(counter.count()).isEqualTo(10.0); + + // test that counter can be increased + underTest.add(10, attributes, Context.root()); + assertThat(counter.count()).isEqualTo(20.0); + + // test that counter cannot be decreased + underTest.add(-5, attributes, Context.root()); + assertThat(counter.count()).isEqualTo(20.0); + + double expectedCount = 20.0; + for (long value : RandomUtils.randomLongs(10, 0L, 500L)) { + expectedCount += value; + + underTest.add(value, attributes, Context.root()); + assertThat(counter.count()).isEqualTo(expectedCount); + } + } + + @Test + void observable() { + AtomicLong atomicLong = new AtomicLong(); + ObservableLongCounter underTest = + MicrometerLongCounter.builder(meterSharedState, "counter") + .setDescription("description") + .setUnit("unit") + .buildWithCallback(measurement -> measurement.record(atomicLong.get())); + + assertThat(callbacks).hasSize(1); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + atomicLong.set(10L); + callbackRegistrar.run(); + FunctionCounter counter = meterRegistry.find("counter").functionCounter(); + assertThat(counter).isNotNull(); + Meter.Id id = counter.getId(); + assertThat(id.getName()).isEqualTo("counter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(counter.count()).isEqualTo(10.0); + + // test that counter can be increased + atomicLong.set(20L); + callbackRegistrar.run(); + assertThat(counter.count()).isEqualTo(20.0); + + // test that counter cannot be decreased + atomicLong.set(5L); + callbackRegistrar.run(); + assertThat(counter.count()).isEqualTo(20.0); + + long value = 20L; + for (long increment : RandomUtils.randomLongs(10, 0L, 500L)) { + value += increment; + atomicLong.set(value); + callbackRegistrar.run(); + assertThat(counter.count()).isEqualTo((double) value); + } + + underTest.close(); + + assertThat(callbacks).isEmpty(); + } + + @Test + void observableWithAttributes() { + AtomicLong atomicLong = new AtomicLong(); + Attributes attributes = Attributes.builder().put("key", "value").build(); + ObservableLongCounter underTest = + MicrometerLongCounter.builder(meterSharedState, "counter") + .setDescription("description") + .setUnit("unit") + .buildWithCallback(measurement -> measurement.record(atomicLong.get(), attributes)); + + assertThat(callbacks).hasSize(1); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + atomicLong.set(10L); + callbackRegistrar.run(); + FunctionCounter counter = meterRegistry.find("counter").functionCounter(); + assertThat(counter).isNotNull(); + Meter.Id id = counter.getId(); + assertThat(id.getName()).isEqualTo("counter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(counter.count()).isEqualTo(10.0); + + // test that counter can be increased + atomicLong.set(20L); + callbackRegistrar.run(); + assertThat(counter.count()).isEqualTo(20.0); + + // test that counter cannot be decreased + atomicLong.set(5L); + callbackRegistrar.run(); + assertThat(counter.count()).isEqualTo(20.0); + + long value = 20L; + for (long increment : RandomUtils.randomLongs(10, 0L, 500L)) { + value += increment; + atomicLong.set(value); + callbackRegistrar.run(); + assertThat(counter.count()).isEqualTo((double) value); + } + + underTest.close(); + + assertThat(callbacks).isEmpty(); + } +} diff --git a/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongGaugeTest.java b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongGaugeTest.java new file mode 100644 index 000000000..18f1a7fa6 --- /dev/null +++ b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongGaugeTest.java @@ -0,0 +1,121 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.ObservableLongGauge; +import io.opentelemetry.contrib.metrics.micrometer.TestCallbackRegistrar; +import io.opentelemetry.contrib.metrics.micrometer.internal.Constants; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterProviderSharedState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class MicrometerLongGaugeTest { + SimpleMeterRegistry meterRegistry; + + List callbacks; + + TestCallbackRegistrar callbackRegistrar; + + MeterProviderSharedState meterProviderSharedState; + + MeterSharedState meterSharedState; + + @BeforeEach + void setUp() { + meterRegistry = new SimpleMeterRegistry(); + callbacks = new ArrayList<>(); + callbackRegistrar = new TestCallbackRegistrar(callbacks); + meterProviderSharedState = new MeterProviderSharedState(() -> meterRegistry, callbackRegistrar); + meterSharedState = new MeterSharedState(meterProviderSharedState, "meter", "1.0", null); + } + + @Test + void observable() { + AtomicLong atomicLong = new AtomicLong(); + ObservableLongGauge underTest = + MicrometerDoubleGauge.builder(meterSharedState, "gauge") + .ofLongs() + .setDescription("description") + .setUnit("unit") + .buildWithCallback(measurement -> measurement.record(atomicLong.get())); + + assertThat(callbacks).hasSize(1); + + atomicLong.set(10L); + callbackRegistrar.run(); + Gauge gauge = meterRegistry.find("gauge").gauge(); + assertThat(gauge).isNotNull(); + Meter.Id id = gauge.getId(); + assertThat(id.getName()).isEqualTo("gauge"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(gauge.value()).isEqualTo(10.0); + + for (long value : RandomUtils.randomLongs(10, -500L, 500L)) { + atomicLong.set(value); + callbackRegistrar.run(); + assertThat(gauge.value()).isEqualTo((double) value); + } + + underTest.close(); + + assertThat(callbacks).isEmpty(); + } + + @Test + void observableWithAttributes() { + AtomicLong atomicLong = new AtomicLong(); + Attributes attributes = Attributes.builder().put("key", "value").build(); + ObservableLongGauge underTest = + MicrometerDoubleGauge.builder(meterSharedState, "gauge") + .ofLongs() + .setDescription("description") + .setUnit("unit") + .buildWithCallback(measurement -> measurement.record(atomicLong.get(), attributes)); + + assertThat(callbacks).hasSize(1); + + atomicLong.set(10L); + callbackRegistrar.run(); + Gauge gauge = meterRegistry.find("gauge").gauge(); + assertThat(gauge).isNotNull(); + Meter.Id id = gauge.getId(); + assertThat(id.getName()).isEqualTo("gauge"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(gauge.value()).isEqualTo(10.0); + + for (long value : RandomUtils.randomLongs(10, -500L, 500L)) { + atomicLong.set(value); + callbackRegistrar.run(); + assertThat(gauge.value()).isEqualTo((double) value); + } + + underTest.close(); + + assertThat(callbacks).isEmpty(); + } +} diff --git a/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongHistogramTest.java b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongHistogramTest.java new file mode 100644 index 000000000..c3e119a59 --- /dev/null +++ b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongHistogramTest.java @@ -0,0 +1,158 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.context.Context; +import io.opentelemetry.contrib.metrics.micrometer.TestCallbackRegistrar; +import io.opentelemetry.contrib.metrics.micrometer.internal.Constants; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterProviderSharedState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class MicrometerLongHistogramTest { + + SimpleMeterRegistry meterRegistry; + + TestCallbackRegistrar callbackRegistrar; + + MeterProviderSharedState meterProviderSharedState; + + MeterSharedState meterSharedState; + + @BeforeEach + void setUp() { + meterRegistry = new SimpleMeterRegistry(); + callbackRegistrar = new TestCallbackRegistrar(Collections.emptyList()); + meterProviderSharedState = new MeterProviderSharedState(() -> meterRegistry, callbackRegistrar); + meterSharedState = new MeterSharedState(meterProviderSharedState, "meter", "1.0", null); + } + + @Test + void add() { + LongHistogram underTest = + MicrometerDoubleHistogram.builder(meterSharedState, "histogram") + .ofLongs() + .setDescription("description") + .setUnit("unit") + .build(); + + underTest.record(10); + + DistributionSummary summary = meterRegistry.find("histogram").summary(); + assertThat(summary).isNotNull(); + Meter.Id id = summary.getId(); + assertThat(id.getName()).isEqualTo("histogram"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(summary.count()).isEqualTo(1); + assertThat(summary.totalAmount()).isEqualTo(10.0); + + long expectedCount = 1; + double expectedTotal = 10.0; + for (long value : RandomUtils.randomLongs(10, 0L, 10L)) { + expectedCount += 1; + expectedTotal += value; + + underTest.record(value); + assertThat(summary.count()).isEqualTo(expectedCount); + assertThat(summary.totalAmount()).isEqualTo(expectedTotal); + } + } + + @Test + void addWithAttributes() { + LongHistogram underTest = + MicrometerDoubleHistogram.builder(meterSharedState, "histogram") + .ofLongs() + .setDescription("description") + .setUnit("unit") + .build(); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + Attributes attributes = Attributes.builder().put("key", "value").build(); + underTest.record(10, attributes); + + DistributionSummary summary = meterRegistry.find("histogram").summary(); + assertThat(summary).isNotNull(); + Meter.Id id = summary.getId(); + assertThat(id.getName()).isEqualTo("histogram"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(summary.count()).isEqualTo(1); + assertThat(summary.totalAmount()).isEqualTo(10.0); + + long expectedCount = 1; + double expectedTotal = 10.0; + for (long value : RandomUtils.randomLongs(10, 0L, 10L)) { + expectedCount += 1; + expectedTotal += value; + + underTest.record(value, attributes); + assertThat(summary.count()).isEqualTo(expectedCount); + assertThat(summary.totalAmount()).isEqualTo(expectedTotal); + } + } + + @Test + void addWithAttributesAndContext() { + LongHistogram underTest = + MicrometerDoubleHistogram.builder(meterSharedState, "histogram") + .ofLongs() + .setDescription("description") + .setUnit("unit") + .build(); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + Attributes attributes = Attributes.builder().put("key", "value").build(); + underTest.record(10, attributes, Context.root()); + + DistributionSummary summary = meterRegistry.find("histogram").summary(); + assertThat(summary).isNotNull(); + Meter.Id id = summary.getId(); + assertThat(id.getName()).isEqualTo("histogram"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(summary.count()).isEqualTo(1); + assertThat(summary.totalAmount()).isEqualTo(10.0); + + long expectedCount = 1; + double expectedTotal = 10.0; + for (long value : RandomUtils.randomLongs(10, 0L, 10L)) { + expectedCount += 1; + expectedTotal += value; + + underTest.record(value, attributes, Context.root()); + assertThat(summary.count()).isEqualTo(expectedCount); + assertThat(summary.totalAmount()).isEqualTo(expectedTotal); + } + } +} diff --git a/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongUpDownCounterTest.java b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongUpDownCounterTest.java new file mode 100644 index 000000000..6404daaf9 --- /dev/null +++ b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/MicrometerLongUpDownCounterTest.java @@ -0,0 +1,271 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.ObservableLongUpDownCounter; +import io.opentelemetry.context.Context; +import io.opentelemetry.contrib.metrics.micrometer.TestCallbackRegistrar; +import io.opentelemetry.contrib.metrics.micrometer.internal.Constants; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterProviderSharedState; +import io.opentelemetry.contrib.metrics.micrometer.internal.state.MeterSharedState; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class MicrometerLongUpDownCounterTest { + + SimpleMeterRegistry meterRegistry; + + List callbacks; + + TestCallbackRegistrar callbackRegistrar; + + MeterProviderSharedState meterProviderSharedState; + + MeterSharedState meterSharedState; + + @BeforeEach + void setUp() { + meterRegistry = new SimpleMeterRegistry(); + callbacks = new ArrayList<>(); + callbackRegistrar = new TestCallbackRegistrar(callbacks); + meterProviderSharedState = new MeterProviderSharedState(() -> meterRegistry, callbackRegistrar); + meterSharedState = new MeterSharedState(meterProviderSharedState, "meter", "1.0", null); + } + + @Test + void add() { + LongUpDownCounter underTest = + MicrometerLongUpDownCounter.builder(meterSharedState, "upDownCounter") + .setDescription("description") + .setUnit("unit") + .build(); + + underTest.add(10); + + Gauge gauge = meterRegistry.find("upDownCounter").gauge(); + assertThat(gauge).isNotNull(); + Meter.Id id = gauge.getId(); + assertThat(id.getName()).isEqualTo("upDownCounter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(gauge.value()).isEqualTo(10.0); + + // test that counter can be increased + underTest.add(10); + assertThat(gauge.value()).isEqualTo(20.0); + + // test that counter can be decreased + underTest.add(-5); + assertThat(gauge.value()).isEqualTo(15.0); + + double expectedCount = 15.0; + for (long value : RandomUtils.randomLongs(10, -500L, 500L)) { + expectedCount += value; + + underTest.add(value); + assertThat(gauge.value()).isEqualTo(expectedCount); + } + } + + @Test + void addWithAttributes() { + LongUpDownCounter underTest = + MicrometerLongUpDownCounter.builder(meterSharedState, "upDownCounter") + .setDescription("description") + .setUnit("unit") + .build(); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + Attributes attributes = Attributes.builder().put("key", "value").build(); + underTest.add(10, attributes); + + Gauge gauge = meterRegistry.find("upDownCounter").gauge(); + assertThat(gauge).isNotNull(); + Meter.Id id = gauge.getId(); + assertThat(id.getName()).isEqualTo("upDownCounter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(gauge.value()).isEqualTo(10.0); + + // test that counter can be increased + underTest.add(10, attributes); + assertThat(gauge.value()).isEqualTo(20.0); + + // test that counter can be decreased + underTest.add(-5, attributes); + assertThat(gauge.value()).isEqualTo(15.0); + + double expectedCount = 15.0; + for (long value : RandomUtils.randomLongs(10, -500L, 500L)) { + expectedCount += value; + + underTest.add(value, attributes); + assertThat(gauge.value()).isEqualTo(expectedCount); + } + } + + @Test + void addWithAttributesAndContext() { + LongUpDownCounter underTest = + MicrometerLongUpDownCounter.builder(meterSharedState, "upDownCounter") + .setDescription("description") + .setUnit("unit") + .build(); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + Attributes attributes = Attributes.builder().put("key", "value").build(); + underTest.add(10, attributes, Context.root()); + + Gauge gauge = meterRegistry.find("upDownCounter").gauge(); + assertThat(gauge).isNotNull(); + Meter.Id id = gauge.getId(); + assertThat(id.getName()).isEqualTo("upDownCounter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(gauge.value()).isEqualTo(10.0); + + // test that counter can be increased + underTest.add(10, attributes, Context.root()); + assertThat(gauge.value()).isEqualTo(20.0); + + // test that counter can be decreased + underTest.add(-5, attributes, Context.root()); + assertThat(gauge.value()).isEqualTo(15.0); + + double expectedCount = 15.0; + for (long value : RandomUtils.randomLongs(10, -500L, 500L)) { + expectedCount += value; + + underTest.add(value, attributes, Context.root()); + assertThat(gauge.value()).isEqualTo(expectedCount); + } + } + + @Test + void observable() { + AtomicLong atomicLong = new AtomicLong(); + ObservableLongUpDownCounter underTest = + MicrometerLongUpDownCounter.builder(meterSharedState, "upDownCounter") + .setDescription("description") + .setUnit("unit") + .buildWithCallback(measurement -> measurement.record(atomicLong.get())); + + assertThat(callbacks).hasSize(1); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + atomicLong.set(10L); + callbackRegistrar.run(); + Gauge gauge = meterRegistry.find("upDownCounter").gauge(); + assertThat(gauge).isNotNull(); + Meter.Id id = gauge.getId(); + assertThat(id.getName()).isEqualTo("upDownCounter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(gauge.value()).isEqualTo(10.0); + + // test that counter can be increased + atomicLong.set(20L); + callbackRegistrar.run(); + assertThat(gauge.value()).isEqualTo(20.0); + + // test that counter can be decreased + atomicLong.set(15L); + callbackRegistrar.run(); + assertThat(gauge.value()).isEqualTo(15.0); + + for (long value : RandomUtils.randomLongs(10, 0L, 500L)) { + atomicLong.set(value); + callbackRegistrar.run(); + assertThat(gauge.value()).isEqualTo((double) value); + } + + underTest.close(); + + assertThat(callbacks).isEmpty(); + } + + @Test + void observableWithAttributes() { + AtomicLong atomicLong = new AtomicLong(); + Attributes attributes = Attributes.builder().put("key", "value").build(); + ObservableLongUpDownCounter underTest = + MicrometerLongUpDownCounter.builder(meterSharedState, "upDownCounter") + .setDescription("description") + .setUnit("unit") + .buildWithCallback(measurement -> measurement.record(atomicLong.get(), attributes)); + + assertThat(callbacks).hasSize(1); + + assertThat(meterRegistry.getMeters()).isEmpty(); + + atomicLong.set(10L); + callbackRegistrar.run(); + Gauge gauge = meterRegistry.find("upDownCounter").gauge(); + assertThat(gauge).isNotNull(); + Meter.Id id = gauge.getId(); + assertThat(id.getName()).isEqualTo("upDownCounter"); + assertThat(id.getTags()) + .containsExactlyInAnyOrder( + Tag.of("key", "value"), + Tag.of(Constants.OTEL_INSTRUMENTATION_NAME, "meter"), + Tag.of(Constants.OTEL_INSTRUMENTATION_VERSION, "1.0")); + assertThat(id.getDescription()).isEqualTo("description"); + assertThat(id.getBaseUnit()).isEqualTo("unit"); + assertThat(gauge.value()).isEqualTo(10.0); + + // test that counter can be increased + atomicLong.set(20L); + callbackRegistrar.run(); + assertThat(gauge.value()).isEqualTo(20.0); + + // test that counter can be decreased + atomicLong.set(15L); + callbackRegistrar.run(); + assertThat(gauge.value()).isEqualTo(15.0); + + for (long value : RandomUtils.randomLongs(10, 0L, 500L)) { + atomicLong.set(value); + callbackRegistrar.run(); + assertThat(gauge.value()).isEqualTo((double) value); + } + + underTest.close(); + + assertThat(callbacks).isEmpty(); + } +} diff --git a/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/RandomUtils.java b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/RandomUtils.java new file mode 100644 index 000000000..aea0a0b3a --- /dev/null +++ b/micrometer-meter-provider/src/test/java/io/opentelemetry/contrib/metrics/micrometer/internal/instruments/RandomUtils.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.metrics.micrometer.internal.instruments; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Stream; + +final class RandomUtils { + + @SuppressWarnings("StreamToIterable") + static Iterable randomDoubles(long size, double origin, double bound) { + Stream stream = ThreadLocalRandom.current().doubles(size, origin, bound).boxed(); + + return stream::iterator; + } + + @SuppressWarnings("StreamToIterable") + static Iterable randomLongs(long size, long origin, long bound) { + Stream stream = ThreadLocalRandom.current().longs(size, origin, bound).boxed(); + + return stream::iterator; + } + + private RandomUtils() {} +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 9855f2401..3527fa5d0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -43,6 +43,7 @@ include(":consistent-sampling") include(":dependencyManagement") include(":example") include(":jfr-streaming") +include(":micrometer-meter-provider") include(":jmx-metrics") include(":maven-extension") include(":runtime-attach")