diff --git a/.github/native-tests.json b/.github/native-tests.json
index cd9b3beb494a91..4d3a6f38156702 100644
--- a/.github/native-tests.json
+++ b/.github/native-tests.json
@@ -117,7 +117,7 @@
{
"category": "Misc4",
"timeout": 130,
- "test-modules": "picocli-native, gradle, micrometer-mp-metrics, micrometer-prometheus, logging-json, jaxp, jaxb, observability-lgtm, opentelemetry, opentelemetry-jdbc-instrumentation, opentelemetry-mongodb-client-instrumentation, opentelemetry-redis-instrumentation, web-dependency-locator",
+ "test-modules": "picocli-native, gradle, micrometer-mp-metrics, micrometer-prometheus, logging-json, jaxp, jaxb, observability-lgtm, opentelemetry, opentelemetry-jdbc-instrumentation, opentelemetry-mongodb-client-instrumentation, opentelemetry-redis-instrumentation, web-dependency-locator, micrometer-opentelemetry",
"os-name": "ubuntu-latest"
},
{
diff --git a/docs/src/main/asciidoc/telemetry-micrometer-to-opentelemetry.adoc b/docs/src/main/asciidoc/telemetry-micrometer-to-opentelemetry.adoc
new file mode 100644
index 00000000000000..38c9dc3c5472a8
--- /dev/null
+++ b/docs/src/main/asciidoc/telemetry-micrometer-to-opentelemetry.adoc
@@ -0,0 +1,219 @@
+////
+This guide is maintained in the main Quarkus repository
+and pull requests should be submitted there:
+https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc
+////
+[id=telemetry-micrometer-opentelemetry]
+= Micrometer and OpenTelemetry extension
+include::_attributes.adoc[]
+:extension-status: preview
+:diataxis-type: reference
+:categories: observability
+:summary: Guide to send Micrometer data to OpenTelemetry.
+:topics: observability,opentelemetry,metrics,micrometer,tracing,logs
+:extensions: io.quarkus:quarkus-micrometer-opentelemetry
+
+This extension provides support for both `Micrometer` and `OpenTelemetry` in Quarkus applications. It streamlines integration by incorporating both extensions along with a bridge that enables sending Micrometer metrics via OpenTelemetry.
+
+include::{includes}/extension-status.adoc[]
+
+[NOTE]
+====
+- The xref:telemetry-micrometer.adoc[Micrometer Guide] is available for detailed information about the Micrometer extension.
+- The xref:opentelemetry.adoc[OpenTelemetry Guide] provides information about the OpenTelemetry extension.
+====
+
+The bridge is more than the simple OTLP registry found in Quarkiverse. In this extension, the OpenTelemetry SDK provides a Micrometer registry implementation.
+
+This allows the normal use of the Micrometer API, but have the metrics handled by the OpenTelemetry extension. All the configurations of the OpenTelemetry extension are available for this bridge and enables forwarding to OpenTelemetry all the automatic instrumentation metrics generated by Micrometer in Quarkus, as well as custom user metrics.
+
+The bridge is based on the https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/micrometer/micrometer-1.5/library[`micrometer/micrometer-1.5`] OpenTelemetry instrumentation library.
+
+== Usage
+
+If you already have your Quarkus project configured, you can add the `quarkus-micrometer-opentelemetry` extension to your project by running the following command in your project base directory:
+
+:add-extension-extensions: micrometer-opentelemetry
+include::{includes}/devtools/extension-add.adoc[]
+
+This will add the following to your build file:
+
+[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
+.pom.xml
+----
+
+ io.quarkus
+ quarkus-micrometer-opentelemetry
+
+----
+
+[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"]
+.build.gradle
+----
+implementation("io.quarkus:quarkus-micrometer-opentelemetry")
+----
+
+== Configuration
+
+Micrometer is enabled by default as are OpenTelemetry tracing, metrics and logs.
+OpenTelemetry metrics auto-instrumentation for HTTP server and JVM metrics are disabled by default because those metrics are already collected by Micrometer.
+
+Particular automated metrics are not enabled by default and can be enabled by setting the, as an example:
+```
+quarkus.micrometer.binder.jvm=true
+```
+on the `application.properties` file.
+
+For this and other properties you can use with the extension, Please refer to:
+
+* xref:telemetry-micrometer.adoc#configuration-reference[Micrometer metrics configurations]
+* xref:opentelemetry.adoc#configuration-reference[OpenTelemetry configurations]
+
+
+
+== Metric differences between Micrometer and OpenTelemetry
+
+=== API differences
+The metrics produced with each framework follow different APIs and the mapping is not 1:1.
+
+One fundamental API difference is that Micrometer uses a https://docs.micrometer.io/micrometer/reference/concepts/timers.html[Timer] and OpenTelemetry uses a https://opentelemetry.io/docs/specs/otel/metrics/data-model/#histogram[Histogram] to record latency (execution time) metrics and the frequency of the events.
+
+When using the `@Timed` annotation with Micrometer, 2 different metrics are https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/324fdbdd452ddffaf2da2c5bf004d8bb3fdfa1dd/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/OpenTelemetryTimer.java#L31[created on the OpenTelemetry side], one `Gauge` for the `max` value and one `Histogram`.
+
+The `DistributionSummary` from Micrometer is transformed into a `histogram` and a `DoubleGauge` for the `max` value. If slos are set in the `DistributionSummary` an addition histogram is created for them.
+
+This table shows the differences between the two frameworks:
+
+|===
+|Micrometer |OpenTelemetry
+
+|DistributionSummary
+|`` (Histogram), `.max` (DoubleGauge)
+
+|DistributionSummary with SLOs
+|`` (Histogram), `.max` (DoubleGauge), `.histogram` (DoubleGauge)
+
+|LongTaskTimer
+|`.active` (ObservableLongUpDownCounter), `.duration` (ObservableDoubleUpDownCounter)
+
+|Timer
+|`` (Histogram), `.max` (ObservableDoubleGauge)
+|===
+
+
+=== Semantic convention differences
+
+The 2 frameworks follow different semantic conventions and approaches to the concept. The OpenTelemetry metrics are based on the https://opentelemetry.io/docs/concepts/semantic-conventions/[`OpenTelemetry Semantic Conventions`] and are still under active development. Micrometer metrics convention format is around for a long time and has not changed much.
+
+When you set these 2 configurations are set in the `application.properties` file:
+
+```
+quarkus.micrometer.binder.jvm=true
+quarkus.micrometer.binder.http-server.enabled=true
+
+```
+The JVM and HTTP server metrics are collected by Micrometer.
+
+Next, are examples of the metrics collected by Micrometer and a comparison of what would be the `quarkus-micrometer-registry-prometheus` output vs the one on this bridge. A link to the equivalent OpenTelemetry Semantic Convention is also provided for reference and is not currently used in the bridge.
+
+|===
+|Micrometer Meter |Quarkus Micrometer Prometheus output | This bridge OpenTelemetry output name | Related OpenTelemetry Semantic Convention (not applied)
+
+|Using the @Timed interceptor.
+|
+|method.timed (Histogram), method.timed.max (DoubleGauge)
+|NA
+
+|Using the @Counted interceptor.
+|
+|method.counted (DoubleSum)
+|NA
+
+|`http.server.active.requests` (Gauge)
+|`http_server_active_requests` (Gauge)
+|`http.server.active.requests` (DoubleGauge)
+|https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserveractive_requests[`http.server.active_requests`] (UpDownCounter)
+
+|`http.server.requests` (Timer)
+|`http_server_requests_seconds_count`, `http_server_requests_seconds_sum`, `http_server_requests_seconds_max` (Gauge)
+|`http.server.requests` (Histogram), `http.server.requests.max` (DoubleGauge)
+|https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestduration[`http.server.request.duration`] (Histogram)
+
+|`http.server.bytes.read` (DistributionSummary)
+|`http_server_bytes_read_count`, `http_server_bytes_read_sum` , `http_server_bytes_read_max` (Gauge)
+|`http.server.bytes.read` (Histogram), `http.server.bytes.read.max` (DoubleGauge)
+|https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestbodysize[`http.server.request.body.size`] (Histogram)
+
+|`http.server.bytes.write` (DistributionSummary)
+|`http_server_bytes_write_count`, `http_server_bytes_write_sum` , `http_server_bytes_write_max` (Gauge)
+|`http.server.bytes.write` (Histogram), `http.server.bytes.write.max` (DoubleGauge)
+|https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverresponsebodysize[`http.server.response.body.size`] (Histogram)
+
+|`http.server.connections` (LongTaskTimer)
+|`http_server_connections_seconds_active_count`, `http_server_connections_seconds_duration_sum` `http_server_connections_seconds_max` (Gauge)
+|`http.server.connections.active` (LongSum), `http.server.connections.duration` (DoubleGauge)
+| N/A
+
+|`jvm.threads.live` (Gauge)
+|`jvm_threads_live_threads` (Gauge)
+|`jvm.threads.live` (DoubleGauge)
+|https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmthreadcount[`jvm.threads.live`] (UpDownCounter)
+
+|`jvm.threads.started` (FunctionCounter)
+|`jvm_threads_started_threads_total` (Counter)
+|`jvm.threads.started` (DoubleSum)
+|https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmthreadcount[`jvm.threads.live`] (UpDownCounter)
+
+|`jvm.threads.daemon` (Gauge)
+|`jvm_threads_daemon_threads` (Gauge)
+|`jvm.threads.daemon` (DoubleGauge)
+|https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmthreadcount[`jvm.threads.live`] (UpDownCounter)
+
+|`jvm.threads.peak` (Gauge)
+|`jvm_threads_peak_threads` (Gauge)
+|`jvm.threads.peak` (DoubleGauge)
+|N/A
+
+|`jvm.threads.states` (Gauge per state)
+|`jvm_threads_states_threads` (Gauge)
+|`jvm.threads.states` (DoubleGauge)
+|https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmthreadcount[`jvm.threads.live`] (UpDownCounter)
+|===
+
+
+[NOTE]
+====
+- Some metrics might be absent of the output if they contain no data.
+====
+
+== See the output
+
+=== Grafana-OTel-LGTM Dev Service
+You can use the xref:observability-devservices-lgtm.adoc[Grafana-OTel-LGTM] devservice.
+
+This Dev service includes a Grafana for visualizing data, Loki to store logs, Tempo to store traces and Prometheus to store metrics.
+Also provides and OTel collector to receive the data.
+
+=== Logging exporter
+
+You can output all metrics to the console by setting the exporter to `logging` in the `application.properties` file:
+[source, properties]
+----
+quarkus.otel.metrics.exporter=logging <1>
+quarkus.otel.metric.export.interval=10000ms <2>
+----
+
+<1> Set the exporter to `logging`.
+Normally you don't need to set this.
+The default is `cdi`.
+<2> Set the interval to export the metrics.
+The default is `1m`, which is too long for debugging.
+
+Also add this dependency to your project:
+[source,xml]
+----
+
+ io.opentelemetry
+ opentelemetry-exporter-logging
+
+----
diff --git a/extensions/micrometer-opentelemetry/deployment/pom.xml b/extensions/micrometer-opentelemetry/deployment/pom.xml
new file mode 100644
index 00000000000000..a500f05fa846a3
--- /dev/null
+++ b/extensions/micrometer-opentelemetry/deployment/pom.xml
@@ -0,0 +1,134 @@
+
+
+ 4.0.0
+
+
+ io.quarkus
+ quarkus-micrometer-opentelemetry-parent
+ 999-SNAPSHOT
+ ../pom.xml
+
+
+ quarkus-micrometer-opentelemetry-deployment
+ Quarkus - Micrometer to OpenTelemetry Bridge - Deployment
+ Micrometer registry implemented by the OpenTelemetry SDK
+
+
+
+
+ io.quarkus
+ quarkus-micrometer-opentelemetry
+ ${project.version}
+
+
+
+ io.quarkus
+ quarkus-micrometer-deployment
+
+
+
+ io.quarkus
+ quarkus-opentelemetry-deployment
+
+
+
+
+ io.quarkus
+ quarkus-junit5-internal
+ test
+
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+ io.opentelemetry
+ opentelemetry-sdk-testing
+ test
+
+
+
+ io.smallrye.reactive
+ smallrye-mutiny-vertx-web-client
+ test
+
+
+
+ io.quarkus
+ quarkus-rest-client-deployment
+ test
+
+
+
+ io.quarkus
+ quarkus-rest-jackson-deployment
+ test
+
+
+
+ io.quarkus
+ quarkus-vertx-http-deployment
+ test
+
+
+
+ io.quarkus
+ quarkus-reactive-routes-deployment
+ test
+
+
+
+
+
+
+ maven-surefire-plugin
+
+
+ org.jboss.logmanager.LogManager
+ INFO
+
+
+
+
+ maven-compiler-plugin
+
+
+ default-compile
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/micrometer-opentelemetry/deployment/src/main/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerOTelBridgeConfigBuilderCustomizer.java b/extensions/micrometer-opentelemetry/deployment/src/main/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerOTelBridgeConfigBuilderCustomizer.java
new file mode 100644
index 00000000000000..3c112f0b2e0f7b
--- /dev/null
+++ b/extensions/micrometer-opentelemetry/deployment/src/main/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerOTelBridgeConfigBuilderCustomizer.java
@@ -0,0 +1,18 @@
+package io.quarkus.micrometer.opentelemetry.deployment;
+
+import io.smallrye.config.SmallRyeConfigBuilder;
+import io.smallrye.config.SmallRyeConfigBuilderCustomizer;
+
+public class MicrometerOTelBridgeConfigBuilderCustomizer implements SmallRyeConfigBuilderCustomizer {
+ @Override
+ public void configBuilder(final SmallRyeConfigBuilder builder) {
+ // // use a priority of 50 to make sure that this is overridable by any of the standard methods
+ // builder.withSources(
+ // new PropertiesConfigSource(Map.of(
+ // "quarkus.otel.metrics.enabled", "true"),
+ // "quarkus-micrometer-opentelemetry", 50),
+ // new PropertiesConfigSource(Map.of(
+ // "quarkus.otel.logs.enabled", "true"),
+ // "quarkus-micrometer-opentelemetry", 50));
+ }
+}
diff --git a/extensions/micrometer-opentelemetry/deployment/src/main/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerOtelBridgeProcessor.java b/extensions/micrometer-opentelemetry/deployment/src/main/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerOtelBridgeProcessor.java
new file mode 100644
index 00000000000000..a3aedcb4f9a0d9
--- /dev/null
+++ b/extensions/micrometer-opentelemetry/deployment/src/main/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerOtelBridgeProcessor.java
@@ -0,0 +1,85 @@
+package io.quarkus.micrometer.opentelemetry.deployment;
+
+import java.util.Locale;
+import java.util.function.BooleanSupplier;
+
+import jakarta.enterprise.inject.Instance;
+import jakarta.inject.Singleton;
+
+import org.jboss.jandex.ClassType;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.ParameterizedType;
+import org.jboss.jandex.Type;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.opentelemetry.api.OpenTelemetry;
+import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.BuildSteps;
+import io.quarkus.deployment.annotations.ExecutionTime;
+import io.quarkus.deployment.annotations.Record;
+import io.quarkus.deployment.builditem.SystemPropertyBuildItem;
+import io.quarkus.micrometer.deployment.MicrometerProcessor;
+import io.quarkus.micrometer.opentelemetry.runtime.MicrometerOtelBridgeRecorder;
+import io.quarkus.opentelemetry.deployment.OpenTelemetryEnabled;
+import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig;
+import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig;
+
+@BuildSteps(onlyIf = {
+ MicrometerProcessor.MicrometerEnabled.class,
+ OpenTelemetryEnabled.class,
+ MicrometerOtelBridgeProcessor.OtlpMetricsExporterEnabled.class })
+public class MicrometerOtelBridgeProcessor {
+
+ //
+ // @BuildStep
+ // public void enableAllOtelSignals(BuildProducer runtimeConfigProducer) {
+ // runtimeConfigProducer.produce(
+ // new RunTimeConfigurationDefaultBuildItem("quarkus.otel.metrics.enabled", "true"));
+ // runtimeConfigProducer.produce(
+ // new RunTimeConfigurationDefaultBuildItem("quarkus.otel.logs.enabled", "true"));
+ // }
+
+ @BuildStep
+ @Record(ExecutionTime.RUNTIME_INIT)
+ SyntheticBeanBuildItem createBridgeBean(OTelRuntimeConfig otelRuntimeConfig,
+ MicrometerOtelBridgeRecorder recorder,
+ BuildProducer systemProperty) {
+
+ // Avoid users from receiving:
+ // io.ope.ins.mic.v1_.OpenTelemetryMeterRegistry] (main) A MeterFilter is being configured ...
+ // registered to this registry...
+ // It's unavoidable because of how Quarkus startup works and users cannot do anything about it.
+ // see: https://github.com/micrometer-metrics/micrometer/issues/4920#issuecomment-2298348202
+ systemProperty.produce(
+ new SystemPropertyBuildItem(
+ "quarkus.log.category.\"io.opentelemetry.instrumentation.micrometer.v1_5.OpenTelemetryMeterRegistry\".level",
+ "ERROR"));
+
+ return SyntheticBeanBuildItem.configure(MeterRegistry.class)
+ .defaultBean()
+ .setRuntimeInit()
+ .unremovable()
+ .scope(Singleton.class)
+ .addInjectionPoint(ParameterizedType.create(DotName.createSimple(Instance.class),
+ new Type[] { ClassType.create(DotName.createSimple(OpenTelemetry.class.getName())) }, null))
+ .createWith(recorder.createBridge(otelRuntimeConfig))
+ .done();
+ }
+
+ /**
+ * No point in activating the bridge if the OTel metrics if off or the exporter is none.
+ */
+ static class OtlpMetricsExporterEnabled implements BooleanSupplier {
+ OTelBuildConfig otelBuildConfig;
+
+ public boolean getAsBoolean() {
+ return otelBuildConfig.metrics().enabled().orElse(Boolean.TRUE) &&
+ !otelBuildConfig.metrics().exporter().stream()
+ .map(exporter -> exporter.toLowerCase(Locale.ROOT))
+ .anyMatch(exporter -> exporter.contains("none"));
+ }
+ }
+
+}
diff --git a/extensions/micrometer-opentelemetry/deployment/src/main/resources/META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer b/extensions/micrometer-opentelemetry/deployment/src/main/resources/META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer
new file mode 100644
index 00000000000000..51c9a69b4249d0
--- /dev/null
+++ b/extensions/micrometer-opentelemetry/deployment/src/main/resources/META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer
@@ -0,0 +1 @@
+io.quarkus.micrometer.opentelemetry.deployment.MicrometerOTelBridgeConfigBuilderCustomizer
\ No newline at end of file
diff --git a/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/DistributionSummaryTest.java b/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/DistributionSummaryTest.java
new file mode 100644
index 00000000000000..741a99eafa8fd4
--- /dev/null
+++ b/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/DistributionSummaryTest.java
@@ -0,0 +1,129 @@
+package io.quarkus.micrometer.opentelemetry.deployment;
+
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.micrometer.core.instrument.DistributionSummary;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.opentelemetry.sdk.metrics.data.MetricData;
+import io.quarkus.micrometer.opentelemetry.deployment.common.InMemoryMetricExporter;
+import io.quarkus.micrometer.opentelemetry.deployment.common.InMemoryMetricExporterProvider;
+import io.quarkus.test.QuarkusUnitTest;
+
+public class DistributionSummaryTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest TEST = new QuarkusUnitTest()
+ .setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addClasses(ManualHistogramBean.class)
+ .addClasses(InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class)
+ .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()),
+ "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider")
+ .add(new StringAsset("""
+ quarkus.otel.metrics.enabled=true\n
+ quarkus.otel.traces.exporter=none\n
+ quarkus.otel.logs.exporter=none\n
+ quarkus.otel.metrics.exporter=in-memory\n
+ quarkus.otel.metric.export.interval=300ms\n
+ quarkus.micrometer.binder-enabled-default=false\n
+ quarkus.micrometer.binder.http-client.enabled=true\n
+ quarkus.micrometer.binder.http-server.enabled=true\n
+ quarkus.micrometer.binder.http-server.match-patterns=/one=/two\n
+ quarkus.micrometer.binder.http-server.ignore-patterns=/two\n
+ quarkus.micrometer.binder.vertx.enabled=true\n
+ pingpong/mp-rest/url=${test.url}\n
+ quarkus.redis.devservices.enabled=false\n
+ """),
+ "application.properties"));
+
+ @Inject
+ ManualHistogramBean manualHistogramBean;
+
+ @Inject
+ InMemoryMetricExporter exporter;
+
+ @Test
+ void histogramTest() {
+ manualHistogramBean.recordHistogram();
+
+ MetricData testSummary = exporter.getLastFinishedHistogramItem("testSummary", 4);
+ assertNotNull(testSummary);
+ assertThat(testSummary)
+ .hasDescription("This is a test distribution summary")
+ .hasUnit("things")
+ .hasHistogramSatisfying(
+ histogram -> histogram.hasPointsSatisfying(
+ points -> points
+ .hasSum(555.5)
+ .hasCount(4)
+ .hasAttributes(attributeEntry("tag", "value"))));
+
+ MetricData textSummaryMax = exporter.getFinishedMetricItem("testSummary.max");
+ assertNotNull(textSummaryMax);
+ assertThat(textSummaryMax)
+ .hasDescription("This is a test distribution summary")
+ .hasDoubleGaugeSatisfying(
+ gauge -> gauge.hasPointsSatisfying(
+ point -> point
+ .hasValue(500)
+ .hasAttributes(attributeEntry("tag", "value"))));
+
+ MetricData testSummaryHistogram = exporter.getFinishedMetricItem("testSummary.histogram"); // present when SLOs are set
+ assertNotNull(testSummaryHistogram);
+ assertThat(testSummaryHistogram)
+ .hasDoubleGaugeSatisfying(
+ gauge -> gauge.hasPointsSatisfying(
+ point -> point
+ .hasValue(1)
+ .hasAttributes(
+ attributeEntry("le", "1"),
+ attributeEntry("tag", "value")),
+ point -> point
+ .hasValue(2)
+ .hasAttributes(
+ attributeEntry("le", "10"),
+ attributeEntry("tag", "value")),
+ point -> point
+ .hasValue(3)
+ .hasAttributes(
+ attributeEntry("le", "100"),
+ attributeEntry("tag", "value")),
+ point -> point
+ .hasValue(4)
+ .hasAttributes(
+ attributeEntry("le", "1000"),
+ attributeEntry("tag", "value"))));
+ }
+
+ @ApplicationScoped
+ public static class ManualHistogramBean {
+ @Inject
+ MeterRegistry registry;
+
+ public void recordHistogram() {
+ DistributionSummary summary = DistributionSummary.builder("testSummary")
+ .description("This is a test distribution summary")
+ .baseUnit("things")
+ .tags("tag", "value")
+ .serviceLevelObjectives(1, 10, 100, 1000)
+ .distributionStatisticBufferLength(10)
+ .register(registry);
+
+ summary.record(0.5);
+ summary.record(5);
+ summary.record(50);
+ summary.record(500);
+ }
+ }
+}
diff --git a/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/MetricsDisabledTest.java b/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/MetricsDisabledTest.java
new file mode 100644
index 00000000000000..dc752e9f373846
--- /dev/null
+++ b/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/MetricsDisabledTest.java
@@ -0,0 +1,77 @@
+package io.quarkus.micrometer.opentelemetry.deployment;
+
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
+import static io.restassured.RestAssured.when;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import jakarta.inject.Inject;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.sdk.metrics.data.MetricData;
+import io.quarkus.micrometer.opentelemetry.deployment.common.InMemoryMetricExporter;
+import io.quarkus.micrometer.opentelemetry.deployment.common.InMemoryMetricExporterProvider;
+import io.quarkus.micrometer.opentelemetry.deployment.common.PingPongResource;
+import io.quarkus.micrometer.opentelemetry.deployment.common.Util;
+import io.quarkus.test.QuarkusUnitTest;
+import io.restassured.RestAssured;
+
+public class MetricsDisabledTest {
+ @RegisterExtension
+ static final QuarkusUnitTest TEST = new QuarkusUnitTest()
+ .setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addClasses(Util.class,
+ PingPongResource.class)
+ .addClasses(InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class)
+ .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()),
+ "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider")
+ .add(new StringAsset("""
+ quarkus.otel.sdk.disabled=true\n
+ quarkus.otel.metrics.enabled=true\n
+ quarkus.otel.traces.exporter=none\n
+ quarkus.otel.logs.exporter=none\n
+ quarkus.otel.metrics.exporter=in-memory\n
+ quarkus.otel.metric.export.interval=300ms\n
+ quarkus.micrometer.binder.http-client.enabled=true\n
+ quarkus.micrometer.binder.http-server.enabled=true\n
+ pingpong/mp-rest/url=${test.url}\n
+ quarkus.redis.devservices.enabled=false\n
+ """),
+ "application.properties"));
+
+ @Inject
+ protected InMemoryMetricExporter metricExporter;
+
+ protected static String mapToString(Map, ?> map) {
+ return (String) map.keySet().stream()
+ .map(key -> "" + key.getKey() + "=" + map.get(key))
+ .collect(Collectors.joining(", ", "{", "}"));
+ }
+
+ @BeforeEach
+ void setUp() {
+ metricExporter.reset();
+ }
+
+ @Test
+ void disabledTest() throws InterruptedException {
+ // The otel metrics are disabled
+ RestAssured.basePath = "/";
+ when().get("/ping/one").then().statusCode(200);
+
+ Thread.sleep(200);
+
+ List metricData = metricExporter.getFinishedMetricItems();
+ assertThat(metricData).isEmpty();
+ }
+}
diff --git a/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/CountedResource.java b/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/CountedResource.java
new file mode 100644
index 00000000000000..29c772a5a403e3
--- /dev/null
+++ b/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/CountedResource.java
@@ -0,0 +1,64 @@
+package io.quarkus.micrometer.opentelemetry.deployment.common;
+
+import static io.quarkus.micrometer.opentelemetry.deployment.compatibility.MicrometerCounterInterceptorTest.*;
+import static java.util.concurrent.CompletableFuture.supplyAsync;
+
+import java.util.concurrent.CompletableFuture;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import io.micrometer.core.annotation.Counted;
+import io.micrometer.core.aop.MeterTag;
+import io.smallrye.mutiny.Uni;
+
+@ApplicationScoped
+public class CountedResource {
+ @Counted(value = "metric.none", recordFailuresOnly = true)
+ public void onlyCountFailures() {
+ }
+
+ @Counted(value = "metric.all", extraTags = { "extra", "tag" })
+ public void countAllInvocations(@MeterTag(key = "do_fail", resolver = TestValueResolver.class) boolean fail) {
+ if (fail) {
+ throw new NullPointerException("Failed on purpose");
+ }
+ }
+
+ @Counted(description = "nice description")
+ public void emptyMetricName(@MeterTag boolean fail) {
+ if (fail) {
+ throw new NullPointerException("Failed on purpose");
+ }
+ }
+
+ @Counted(value = "async.none", recordFailuresOnly = true)
+ public CompletableFuture> onlyCountAsyncFailures(GuardedResult guardedResult) {
+ return supplyAsync(guardedResult::get);
+ }
+
+ @Counted(value = "async.all", extraTags = { "extra", "tag" })
+ public CompletableFuture> countAllAsyncInvocations(GuardedResult guardedResult) {
+ return supplyAsync(guardedResult::get);
+ }
+
+ @Counted
+ public CompletableFuture> emptyAsyncMetricName(GuardedResult guardedResult) {
+ return supplyAsync(guardedResult::get);
+ }
+
+ @Counted(value = "uni.none", recordFailuresOnly = true)
+ public Uni> onlyCountUniFailures(GuardedResult guardedResult) {
+ return Uni.createFrom().item(guardedResult::get);
+ }
+
+ @Counted(value = "uni.all", extraTags = { "extra", "tag" })
+ public Uni> countAllUniInvocations(GuardedResult guardedResult) {
+ return Uni.createFrom().item(guardedResult::get);
+ }
+
+ @Counted
+ public Uni> emptyUniMetricName(GuardedResult guardedResult) {
+ return Uni.createFrom().item(guardedResult::get);
+ }
+
+}
diff --git a/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/GuardedResult.java b/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/GuardedResult.java
new file mode 100644
index 00000000000000..642bde50ba8ffa
--- /dev/null
+++ b/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/GuardedResult.java
@@ -0,0 +1,34 @@
+package io.quarkus.micrometer.opentelemetry.deployment.common;
+
+public class GuardedResult {
+
+ private boolean complete;
+ private NullPointerException withException;
+
+ public synchronized Object get() {
+ while (!complete) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ // Intentionally empty
+ }
+ }
+
+ if (withException == null) {
+ return new Object();
+ }
+
+ throw withException;
+ }
+
+ public synchronized void complete() {
+ complete(null);
+ }
+
+ public synchronized void complete(NullPointerException withException) {
+ this.complete = true;
+ this.withException = withException;
+ notifyAll();
+ }
+
+}
diff --git a/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/HelloResource.java b/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/HelloResource.java
new file mode 100644
index 00000000000000..a7d949f2ddac26
--- /dev/null
+++ b/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/HelloResource.java
@@ -0,0 +1,23 @@
+package io.quarkus.micrometer.opentelemetry.deployment.common;
+
+import jakarta.inject.Singleton;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.OPTIONS;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+
+@Path("/hello")
+@Singleton
+public class HelloResource {
+ @GET
+ @Path("{message}")
+ public String hello(@PathParam("message") String message) {
+ return "hello " + message;
+ }
+
+ @OPTIONS
+ @Path("{message}")
+ public String helloOptions(@PathParam("message") String message) {
+ return "hello " + message;
+ }
+}
diff --git a/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/InMemoryMetricExporter.java b/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/InMemoryMetricExporter.java
new file mode 100644
index 00000000000000..d484cde3fd8705
--- /dev/null
+++ b/extensions/micrometer-opentelemetry/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/InMemoryMetricExporter.java
@@ -0,0 +1,178 @@
+package io.quarkus.micrometer.opentelemetry.deployment.common;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Assertions;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.metrics.InstrumentType;
+import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
+import io.opentelemetry.sdk.metrics.data.MetricData;
+import io.opentelemetry.sdk.metrics.data.PointData;
+import io.opentelemetry.sdk.metrics.export.MetricExporter;
+import io.opentelemetry.semconv.SemanticAttributes;
+import io.quarkus.arc.Unremovable;
+
+@Unremovable
+@ApplicationScoped
+public class InMemoryMetricExporter implements MetricExporter {
+
+ private final Queue finishedMetricItems = new ConcurrentLinkedQueue<>();
+ private final AggregationTemporality aggregationTemporality = AggregationTemporality.CUMULATIVE;
+ private boolean isStopped = false;
+
+ public MetricDataFilter metrics(final String name) {
+ return new MetricDataFilter(this, name);
+ }
+
+ public MetricDataFilter get(final String name) {
+ return new MetricDataFilter(this, name);
+ }
+
+ public MetricDataFilter find(final String name) {
+ return new MetricDataFilter(this, name);
+ }
+
+ /*
+ * ignore points with /export in the route
+ */
+ private static boolean notExporterPointData(PointData pointData) {
+ return pointData.getAttributes().asMap().entrySet().stream()
+ .noneMatch(entry -> entry.getKey().getKey().equals(SemanticAttributes.HTTP_ROUTE.getKey()) &&
+ entry.getValue().toString().contains("/export"));
+ }
+
+ private static boolean isPathFound(String path, Attributes attributes) {
+ if (path == null) {
+ return true;// any match
+ }
+ Object value = attributes.asMap().get(AttributeKey.stringKey(SemanticAttributes.HTTP_ROUTE.getKey()));
+ if (value == null) {
+ return false;
+ }
+ return value.toString().equals(path);
+ }
+
+ public MetricData getLastFinishedHistogramItem(String testSummary, int count) {
+ Awaitility.await().atMost(5, SECONDS)
+ .untilAsserted(() -> Assertions.assertEquals(count, getFinishedMetricItems(testSummary, null).size()));
+ List metricData = getFinishedMetricItems(testSummary, null);
+ return metricData.get(metricData.size() - 1);// get last added entry which will be the most recent
+ }
+
+ public void assertCountDataPointsAtLeast(final String name, final String target, final int count) {
+ Awaitility.await().atMost(5, SECONDS)
+ .untilAsserted(() -> Assertions.assertTrue(count < countMaxPoints(name, target)));
+ }
+
+ public void assertCountDataPointsAtLeastOrEqual(final String name, final String target, final int count) {
+ Awaitility.await().atMost(5, SECONDS)
+ .untilAsserted(() -> Assertions.assertTrue(count <= countMaxPoints(name, target)));
+ }
+
+ public void assertCountDataPointsAtLeastOrEqual(Supplier tag, int count) {
+ Awaitility.await().atMost(50, SECONDS)
+ .untilAsserted(() -> Assertions.assertTrue(count <= tag.get().lastReadingPointsSize()));
+ }
+
+ private Integer countMaxPoints(String name, String target) {
+ List metricData = getFinishedMetricItems(name, target);
+ if (metricData.isEmpty()) {
+ return 0;
+ }
+ int size = metricData.get(metricData.size() - 1).getData().getPoints().size();
+ return size;
+ }
+
+ /**
+ * Returns a {@code List} of the finished {@code Metric}s, represented by {@code MetricData}.
+ *
+ * @return a {@code List} of the finished {@code Metric}s.
+ */
+ public List getFinishedMetricItems() {
+ return Collections.unmodifiableList(new ArrayList<>(finishedMetricItems));
+ }
+
+ public MetricData getFinishedMetricItem(String metricName) {
+ List metricData = getFinishedMetricItems(metricName, null);
+ return metricData.get(metricData.size() - 1);// get last added entry which will be the most recent
+ }
+
+ public List getFinishedMetricItems(final String name, final String target) {
+ return Collections.unmodifiableList(new ArrayList<>(
+ finishedMetricItems.stream()
+ .filter(metricData -> metricData.getName().equals(name))
+ .filter(metricData -> metricData.getData().getPoints().stream()
+ .anyMatch(point -> isPathFound(target, point.getAttributes())))
+ .collect(Collectors.toList())));
+ }
+
+ /**
+ * Clears the internal {@code List} of finished {@code Metric}s.
+ *
+ *
+ * Does not reset the state of this exporter if already shutdown.
+ */
+ public void reset() {
+ finishedMetricItems.clear();
+ }
+
+ @Override
+ public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) {
+ return aggregationTemporality;
+ }
+
+ /**
+ * Exports the collection of {@code Metric}s into the inmemory queue.
+ *
+ *
+ * If this is called after {@code shutdown}, this will return {@code ResultCode.FAILURE}.
+ */
+ @Override
+ public CompletableResultCode export(Collection metrics) {
+ if (isStopped) {
+ return CompletableResultCode.ofFailure();
+ }
+ finishedMetricItems.addAll(metrics);
+ return CompletableResultCode.ofSuccess();
+ }
+
+ /**
+ * The InMemory exporter does not batch metrics, so this method will immediately return with
+ * success.
+ *
+ * @return always Success
+ */
+ @Override
+ public CompletableResultCode flush() {
+ return CompletableResultCode.ofSuccess();
+ }
+
+ /**
+ * Clears the internal {@code List} of finished {@code Metric}s.
+ *
+ *