Repository: quarkusio/quarkus
Pull request #45522: WebSockets Next: use SPI to separate the Websockets extension from OpenTelemetry and Micrometer extensions
Author: login: michalvavrik
Labels: name: area/core
name: area/dependencies
description: Pull requests that update a dependency file
name: area/tracing
name: area/metrics
name: area/websockets
name: triage/flaky-test
Created: 2025-01-13T00:17:32Z | Merged: 2025-05-02T11:09:52Z

- https://github.com/quarkusio/quarkus/pull/44379#pullrequestreview-2451631566


Diff:
diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index 8f8c02ecb6f41..b3a401218b4d6 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -2359,6 +2359,11 @@
                 <artifactId>quarkus-websockets-next-kotlin</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>io.quarkus</groupId>
+                <artifactId>quarkus-websockets-next-spi</artifactId>
+                <version>${project.version}</version>
+            </dependency>
             <dependency>
                 <groupId>io.quarkus</groupId>
                 <artifactId>quarkus-undertow-spi</artifactId>
diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/WebSocketsBinderProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/WebSocketsBinderProcessor.java
new file mode 100644
index 0000000000000..1ede0da31ac57
--- /dev/null
+++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/WebSocketsBinderProcessor.java
@@ -0,0 +1,27 @@
+package io.quarkus.micrometer.deployment.binder;
+
+import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
+import io.quarkus.deployment.Capabilities;
+import io.quarkus.deployment.Capability;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.BuildSteps;
+import io.quarkus.micrometer.deployment.MicrometerProcessor;
+import io.quarkus.micrometer.runtime.binder.websockets.WebSocketMetricsInterceptorProducerImpl;
+
+/**
+ * Add support for WebSockets Next instrumentation.
+ */
+@BuildSteps(onlyIf = MicrometerProcessor.MicrometerEnabled.class)
+public class WebSocketsBinderProcessor {
+
+    @BuildStep
+    void registerWebSocketMetricsInterceptor(BuildProducer<AdditionalBeanBuildItem> additionalBeanProducer,
+            Capabilities capabilities) {
+        if (capabilities.isPresent(Capability.WEBSOCKETS_NEXT)) {
+            additionalBeanProducer
+                    .produce(AdditionalBeanBuildItem.unremovableOf(WebSocketMetricsInterceptorProducerImpl.class));
+        }
+    }
+
+}
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryConstants.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/websockets/WebSocketMetricConstants.java
similarity index 85%
rename from extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryConstants.java
rename to extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/websockets/WebSocketMetricConstants.java
index d841b501cf422..b5282de4f6f2e 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryConstants.java
+++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/websockets/WebSocketMetricConstants.java
@@ -1,18 +1,11 @@
-package io.quarkus.websockets.next.runtime.telemetry;
+package io.quarkus.micrometer.runtime.binder.websockets;
 
-public final class TelemetryConstants {
+public final class WebSocketMetricConstants {
 
-    private TelemetryConstants() {
+    private WebSocketMetricConstants() {
         // class with constants
     }
 
-    /**
-     * OpenTelemetry attributes added to spans created for opened and closed connections.
-     */
-    public static final String CONNECTION_ID_ATTR_KEY = "connection.id";
-    public static final String CONNECTION_ENDPOINT_ATTR_KEY = "connection.endpoint.id";
-    public static final String CONNECTION_CLIENT_ATTR_KEY = "connection.client.id";
-
     /**
      * Counts all the WebSockets client opened connections.
      */
diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/websockets/WebSocketMetricsInterceptorProducerImpl.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/websockets/WebSocketMetricsInterceptorProducerImpl.java
new file mode 100644
index 0000000000000..06aecf2f7dac0
--- /dev/null
+++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/websockets/WebSocketMetricsInterceptorProducerImpl.java
@@ -0,0 +1,137 @@
+package io.quarkus.micrometer.runtime.binder.websockets;
+
+import static io.quarkus.micrometer.runtime.binder.websockets.WebSocketMetricConstants.DIRECTION_TAG_KEY;
+import static io.quarkus.micrometer.runtime.binder.websockets.WebSocketMetricConstants.Direction.INBOUND;
+import static io.quarkus.micrometer.runtime.binder.websockets.WebSocketMetricConstants.Direction.OUTBOUND;
+
+import jakarta.enterprise.context.Dependent;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketMetricsInterceptorProducer;
+
+@Dependent
+public final class WebSocketMetricsInterceptorProducerImpl implements WebSocketMetricsInterceptorProducer {
+
+    private static final String URI_TAG_KEY = "uri";
+    private final MeterRegistry meterRegistry;
+
+    WebSocketMetricsInterceptorProducerImpl(MeterRegistry meterRegistry) {
+        this.meterRegistry = meterRegistry;
+    }
+
+    @Override
+    public WebSocketMetricsInterceptor createServerMetricsInterceptor() {
+        final Meter.MeterProvider<Counter> messagesCounter = Counter
+                .builder(WebSocketMetricConstants.SERVER_COUNT)
+                .description("Number of messages sent and received by server endpoints.")
+                .withRegistry(meterRegistry);
+        final Meter.MeterProvider<Counter> bytesCounter = Counter
+                .builder(WebSocketMetricConstants.SERVER_BYTES)
+                .description("Number of bytes sent and received by server endpoints.")
+                .withRegistry(meterRegistry);
+        final Meter.MeterProvider<Counter> closedConnectionCounter = Counter
+                .builder(WebSocketMetricConstants.SERVER_CONNECTION_CLOSED)
+                .description("Number of closed server WebSocket connections.")
+                .withRegistry(meterRegistry);
+        final Meter.MeterProvider<Counter> serverErrorsCounter = Counter
+                .builder(WebSocketMetricConstants.SERVER_ENDPOINT_COUNT_ERRORS)
+                .description("Counts all the WebSockets server endpoint errors.")
+                .withRegistry(meterRegistry);
+        final Meter.MeterProvider<Counter> connectionOpenCounter = Counter
+                .builder(WebSocketMetricConstants.SERVER_CONNECTION_OPENED)
+                .description("Number of opened server connections.")
+                .withRegistry(meterRegistry);
+        final Meter.MeterProvider<Counter> connectionOpeningFailedCounter = Counter
+                .builder(WebSocketMetricConstants.SERVER_CONNECTION_ON_OPEN_ERROR)
+                .description("Number of failures occurred when opening server connection failed.")
+                .withRegistry(meterRegistry);
+        return new WebSocketMetricsInterceptorImpl(messagesCounter, bytesCounter, closedConnectionCounter, serverErrorsCounter,
+                connectionOpenCounter, connectionOpeningFailedCounter);
+    }
+
+    @Override
+    public WebSocketMetricsInterceptor createClientMetricsInterceptor() {
+        final Meter.MeterProvider<Counter> messagesCounter = Counter
+                .builder(WebSocketMetricConstants.CLIENT_COUNT)
+                .description("Number of messages sent and received by client endpoints.")
+                .withRegistry(meterRegistry);
+        final Meter.MeterProvider<Counter> bytesCounter = Counter
+                .builder(WebSocketMetricConstants.CLIENT_BYTES)
+                .description("Number of bytes sent and received by client endpoints.")
+                .withRegistry(meterRegistry);
+        final Meter.MeterProvider<Counter> closedConnectionCounter = Counter
+                .builder(WebSocketMetricConstants.CLIENT_CONNECTION_CLOSED)
+                .description("Number of closed client WebSocket connections.")
+                .withRegistry(meterRegistry);
+        final Meter.MeterProvider<Counter> clientErrorsCounter = Counter
+                .builder(WebSocketMetricConstants.CLIENT_ENDPOINT_COUNT_ERRORS)
+                .description("Counts all the WebSockets client endpoint errors.")
+                .withRegistry(meterRegistry);
+        final Meter.MeterProvider<Counter> connectionOpenCounter = Counter
+                .builder(WebSocketMetricConstants.CLIENT_CONNECTION_OPENED)
+                .description("Number of opened client connections.")
+                .withRegistry(meterRegistry);
+        final Meter.MeterProvider<Counter> connectionOpeningFailedCounter = Counter
+                .builder(WebSocketMetricConstants.CLIENT_CONNECTION_OPENED_ERROR)
+                .description("Number of failures occurred when opening client connection failed.")
+                .withRegistry(meterRegistry);
+        return new WebSocketMetricsInterceptorImpl(messagesCounter, bytesCounter, closedConnectionCounter, clientErrorsCounter,
+                connectionOpenCounter, connectionOpeningFailedCounter);
+    }
+
+    private static final class WebSocketMetricsInterceptorImpl implements WebSocketMetricsInterceptor {
+
+        private final Meter.MeterProvider<Counter> messagesCounter;
+        private final Meter.MeterProvider<Counter> bytesCounter;
+        private final Meter.MeterProvider<Counter> closedConnectionCounter;
+        private final Meter.MeterProvider<Counter> errorsCounter;
+        private final Meter.MeterProvider<Counter> connectionOpenCounter;
+        private final Meter.MeterProvider<Counter> connectionOpeningFailedCounter;
+
+        private WebSocketMetricsInterceptorImpl(Meter.MeterProvider<Counter> messagesCounter,
+                Meter.MeterProvider<Counter> bytesCounter, Meter.MeterProvider<Counter> closedConnectionCounter,
+                Meter.MeterProvider<Counter> errorsCounter, Meter.MeterProvider<Counter> connectionOpenCounter,
+                Meter.MeterProvider<Counter> connectionOpeningFailedCounter) {
+            this.messagesCounter = messagesCounter;
+            this.bytesCounter = bytesCounter;
+            this.closedConnectionCounter = closedConnectionCounter;
+            this.errorsCounter = errorsCounter;
+            this.connectionOpenCounter = connectionOpenCounter;
+            this.connectionOpeningFailedCounter = connectionOpeningFailedCounter;
+        }
+
+        @Override
+        public void onError(String route) {
+            errorsCounter.withTag(URI_TAG_KEY, route).increment();
+        }
+
+        @Override
+        public void onMessageSent(byte[] data, String route) {
+            messagesCounter.withTags(URI_TAG_KEY, route, DIRECTION_TAG_KEY, OUTBOUND.toString()).increment();
+            bytesCounter.withTags(URI_TAG_KEY, route, DIRECTION_TAG_KEY, OUTBOUND.toString()).increment(data.length);
+        }
+
+        @Override
+        public void onMessageReceived(byte[] data, String route) {
+            messagesCounter.withTags(URI_TAG_KEY, route, DIRECTION_TAG_KEY, INBOUND.toString()).increment();
+            bytesCounter.withTags(URI_TAG_KEY, route, DIRECTION_TAG_KEY, INBOUND.toString()).increment(data.length);
+        }
+
+        @Override
+        public void onConnectionOpened(String route) {
+            connectionOpenCounter.withTag(URI_TAG_KEY, route).increment();
+        }
+
+        @Override
+        public void onConnectionOpeningFailed(String route) {
+            connectionOpeningFailedCounter.withTag(URI_TAG_KEY, route).increment();
+        }
+
+        @Override
+        public void onConnectionClosed(String route) {
+            closedConnectionCounter.withTag(URI_TAG_KEY, route).increment();
+        }
+    }
+}
diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java
index 1c9928aa491b1..a223d10ea92d2 100644
--- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java
+++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java
@@ -49,6 +49,7 @@
 import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType;
 import io.quarkus.opentelemetry.runtime.tracing.TracerRecorder;
 import io.quarkus.opentelemetry.runtime.tracing.cdi.TracerProducer;
+import io.quarkus.opentelemetry.runtime.tracing.intrumentation.websockets.WebSocketTracesInterceptorImpl;
 import io.quarkus.opentelemetry.runtime.tracing.security.EndUserSpanProcessor;
 import io.quarkus.opentelemetry.runtime.tracing.security.SecurityEventUtil;
 import io.quarkus.vertx.http.deployment.spi.FrameworkEndpointsBuildItem;
@@ -220,6 +221,14 @@ void registerEndUserAttributesEventObserver(Capabilities capabilities,
         }
     }
 
+    @BuildStep
+    void registerWebSocketTracesInterceptor(BuildProducer<AdditionalBeanBuildItem> additionalBeanProducer,
+            Capabilities capabilities) {
+        if (capabilities.isPresent(Capability.WEBSOCKETS_NEXT)) {
+            additionalBeanProducer.produce(AdditionalBeanBuildItem.unremovableOf(WebSocketTracesInterceptorImpl.class));
+        }
+    }
+
     private static ObserverConfiguratorBuildItem createEventObserver(
             ObserverRegistrationPhaseBuildItem observerRegistrationPhase, SecurityEventType eventType, String utilMethodName) {
         return new ObserverConfiguratorBuildItem(observerRegistrationPhase.getContext()
diff --git a/extensions/opentelemetry/runtime/pom.xml b/extensions/opentelemetry/runtime/pom.xml
index e262526d857a3..bcda8e7ab514b 100644
--- a/extensions/opentelemetry/runtime/pom.xml
+++ b/extensions/opentelemetry/runtime/pom.xml
@@ -52,6 +52,10 @@
             <artifactId>nativeimage</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>io.quarkus</groupId>
+            <artifactId>quarkus-websockets-next-spi</artifactId>
+        </dependency>
 
         <!-- Optional Dependencies -->
         <dependency>
diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/websockets/WebSocketTracesInterceptorImpl.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/websockets/WebSocketTracesInterceptorImpl.java
new file mode 100644
index 0000000000000..c9566d466d1df
--- /dev/null
+++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/websockets/WebSocketTracesInterceptorImpl.java
@@ -0,0 +1,124 @@
+package io.quarkus.opentelemetry.runtime.tracing.intrumentation.websockets;
+
+import java.util.Map;
+
+import jakarta.enterprise.context.Dependent;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.semconv.UrlAttributes;
+import io.quarkus.websockets.next.runtime.spi.telemetry.EndpointKind;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketEndpointContext;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketTracesInterceptor;
+
+@Dependent
+public final class WebSocketTracesInterceptorImpl implements WebSocketTracesInterceptor {
+
+    /**
+     * OpenTelemetry attributes added to spans created for opened and closed connections.
+     */
+    public static final String CONNECTION_ID_ATTR_KEY = "connection.id";
+    public static final String CONNECTION_ENDPOINT_ATTR_KEY = "connection.endpoint.id";
+    public static final String CONNECTION_CLIENT_ATTR_KEY = "connection.client.id";
+    private static final String CONNECTION_OPENED_SPAN_CTX = "io.quarkus.opentelemetry.ws.connection-opened-span-ctx";
+
+    private final Tracer tracer;
+
+    WebSocketTracesInterceptorImpl(Tracer tracer) {
+        this.tracer = tracer;
+    }
+
+    @Override
+    public Map<String, Object> onConnectionOpened(String route, EndpointKind endpointKind) {
+        var span = tracer.spanBuilder("OPEN " + route)
+                .setSpanKind(getSpanKind(endpointKind))
+                .addLink(previousSpanContext())
+                .setAttribute(UrlAttributes.URL_PATH, route)
+                .startSpan();
+        try (var ignored = span.makeCurrent()) {
+            return Map.of(CONNECTION_OPENED_SPAN_CTX, span.getSpanContext());
+        } finally {
+            span.end();
+        }
+    }
+
+    @Override
+    public void onConnectionOpeningFailed(Throwable cause, String route, EndpointKind endpointKind,
+            Map<String, Object> connectionOpenedContext) {
+        tracer.spanBuilder("OPEN " + route)
+                .setSpanKind(getSpanKind(endpointKind))
+                .addLink(getOnOpenSpanContext(connectionOpenedContext))
+                .setAttribute(UrlAttributes.URL_PATH, route)
+                .startSpan()
+                .recordException(cause)
+                .end();
+    }
+
+    @Override
+    public void onConnectionClosed(WebSocketEndpointContext ctx) {
+        tracer.spanBuilder("CLOSE " + ctx.route())
+                .setSpanKind(getSpanKind(ctx))
+                .addLink(getOnOpenSpanContext(ctx))
+                .setAttribute(CONNECTION_ID_ATTR_KEY, ctx.connectionId())
+                .setAttribute(UrlAttributes.URL_PATH, ctx.route())
+                .setAttribute(getTargetIdKey(ctx), ctx.targetId())
+                .startSpan()
+                .end();
+    }
+
+    @Override
+    public void onConnectionClosingFailed(Throwable throwable, WebSocketEndpointContext ctx) {
+        tracer.spanBuilder("CLOSE " + ctx.route())
+                .setSpanKind(getSpanKind(ctx))
+                .addLink(getOnOpenSpanContext(ctx))
+                .setAttribute(CONNECTION_ID_ATTR_KEY, ctx.connectionId())
+                .setAttribute(UrlAttributes.URL_PATH, ctx.route())
+                .setAttribute(getTargetIdKey(ctx), ctx.targetId())
+                .startSpan()
+                .recordException(throwable)
+                .end();
+    }
+
+    private static SpanContext getOnOpenSpanContext(WebSocketEndpointContext ctx) {
+        return getOnOpenSpanContext(ctx.connectionContextStorage());
+    }
+
+    private static SpanContext getOnOpenSpanContext(Map<String, Object> connectionContextStorage) {
+        if (connectionContextStorage == null) {
+            return null;
+        }
+        return (SpanContext) connectionContextStorage.get(CONNECTION_OPENED_SPAN_CTX);
+    }
+
+    private static String getTargetIdKey(WebSocketEndpointContext ctx) {
+        return getTargetIdKey(ctx.endpointKind());
+    }
+
+    private static String getTargetIdKey(EndpointKind endpointKind) {
+        return switch (endpointKind) {
+            case CLIENT -> CONNECTION_CLIENT_ATTR_KEY;
+            case SERVER -> CONNECTION_ENDPOINT_ATTR_KEY;
+        };
+    }
+
+    private static SpanKind getSpanKind(WebSocketEndpointContext context) {
+        return getSpanKind(context.endpointKind());
+    }
+
+    private static SpanKind getSpanKind(EndpointKind endpointKind) {
+        return switch (endpointKind) {
+            case CLIENT -> SpanKind.CLIENT;
+            case SERVER -> SpanKind.SERVER;
+        };
+    }
+
+    private static SpanContext previousSpanContext() {
+        var span = Span.current();
+        if (span.getSpanContext().isValid()) {
+            return span.getSpanContext();
+        }
+        return null;
+    }
+}
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MetricsAsserter.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MetricsAsserter.java
index 69ba97eb5efb9..5ee5729902e43 100644
--- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MetricsAsserter.java
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MetricsAsserter.java
@@ -1,19 +1,8 @@
 package io.quarkus.websockets.next.test.telemetry;
 
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CLIENT_BYTES;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CLIENT_CONNECTION_CLOSED;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CLIENT_CONNECTION_OPENED;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CLIENT_COUNT;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CLIENT_ENDPOINT_COUNT_ERRORS;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_BYTES;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_CLOSED;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_ON_OPEN_ERROR;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_OPENED;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_COUNT;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_ENDPOINT_COUNT_ERRORS;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.Direction.INBOUND;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.Direction.OUTBOUND;
 import static io.quarkus.websockets.next.test.telemetry.AbstractWebSocketsOnMessageTest.getMetrics;
+import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.Direction.INBOUND;
+import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.Direction.OUTBOUND;
 
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
@@ -24,10 +13,25 @@
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
 
-import io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.Direction;
-
 public final class MetricsAsserter {
 
+    private static final String CLIENT_CONNECTION_OPENED = "quarkus.websockets.client.connections.opened";
+    private static final String SERVER_CONNECTION_OPENED = "quarkus.websockets.server.connections.opened";
+    private static final String SERVER_CONNECTION_ON_OPEN_ERROR = "quarkus.websockets.server.connections.onopen.errors";
+    private static final String CLIENT_CONNECTION_CLOSED = "quarkus.websockets.client.connections.closed";
+    private static final String SERVER_CONNECTION_CLOSED = "quarkus.websockets.server.connections.closed";
+    private static final String SERVER_ENDPOINT_COUNT_ERRORS = "quarkus.websockets.server.endpoint.count.errors";
+    private static final String CLIENT_ENDPOINT_COUNT_ERRORS = "quarkus.websockets.client.endpoint.count.errors";
+    private static final String SERVER_COUNT = "quarkus.websockets.server.count";
+    private static final String SERVER_BYTES = "quarkus.websockets.server.bytes";
+    private static final String CLIENT_COUNT = "quarkus.websockets.client.count";
+    private static final String CLIENT_BYTES = "quarkus.websockets.client.bytes";
+
+    public enum Direction {
+        INBOUND,
+        OUTBOUND
+    }
+
     int serverSentCount;
     int serverReceivedCount;
     int serverReceivedCountBytes;
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/OpenTelemetryWebSocketsTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/OpenTelemetryWebSocketsTest.java
index c55bde343fa92..b369f2b41eb3c 100644
--- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/OpenTelemetryWebSocketsTest.java
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/OpenTelemetryWebSocketsTest.java
@@ -1,9 +1,6 @@
 package io.quarkus.websockets.next.test.telemetry;
 
 import static io.opentelemetry.semconv.UrlAttributes.URL_PATH;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CONNECTION_CLIENT_ATTR_KEY;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CONNECTION_ENDPOINT_ATTR_KEY;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CONNECTION_ID_ATTR_KEY;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -176,13 +173,13 @@ public void testServerTracesWhenErrorOnMessage() {
     private String getConnectionIdAttrVal(SpanData connectionOpenedSpan) {
         return connectionOpenedSpan
                 .getAttributes()
-                .get(AttributeKey.stringKey(CONNECTION_ID_ATTR_KEY));
+                .get(AttributeKey.stringKey("connection.id"));
     }
 
     private String getClientIdAttrVal(SpanData connectionOpenedSpan) {
         return connectionOpenedSpan
                 .getAttributes()
-                .get(AttributeKey.stringKey(CONNECTION_CLIENT_ATTR_KEY));
+                .get(AttributeKey.stringKey("connection.client.id"));
     }
 
     private String getUriAttrVal(SpanData connectionOpenedSpan) {
@@ -192,7 +189,7 @@ private String getUriAttrVal(SpanData connectionOpenedSpan) {
     private String getEndpointIdAttrVal(SpanData connectionOpenedSpan) {
         return connectionOpenedSpan
                 .getAttributes()
-                .get(AttributeKey.stringKey(CONNECTION_ENDPOINT_ATTR_KEY));
+                .get(AttributeKey.stringKey("connection.endpoint.id"));
     }
 
     private void waitForTracesToArrive(int expectedTracesCount) {
diff --git a/extensions/websockets-next/pom.xml b/extensions/websockets-next/pom.xml
index 623739b4ed3c7..78ce8941fe1dd 100644
--- a/extensions/websockets-next/pom.xml
+++ b/extensions/websockets-next/pom.xml
@@ -19,6 +19,7 @@
         <module>deployment</module>
         <module>runtime</module>
         <module>kotlin</module>
+        <module>spi</module>
     </modules>
 
 </project>
diff --git a/extensions/websockets-next/runtime/pom.xml b/extensions/websockets-next/runtime/pom.xml
index 4a5a6a3c56ac1..7c8cd116092df 100644
--- a/extensions/websockets-next/runtime/pom.xml
+++ b/extensions/websockets-next/runtime/pom.xml
@@ -38,28 +38,15 @@
             <groupId>io.quarkus</groupId>
             <artifactId>quarkus-websockets-next-kotlin</artifactId>
         </dependency>
+        <dependency>
+            <groupId>io.quarkus</groupId>
+            <artifactId>quarkus-websockets-next-spi</artifactId>
+        </dependency>
         <!-- Quarkus Security API -->
         <dependency>
             <groupId>io.quarkus.security</groupId>
             <artifactId>quarkus-security</artifactId>
         </dependency>
-        <!-- Dependencies used for integration with OpenTelemetry extension -->
-        <dependency>
-            <groupId>io.opentelemetry</groupId>
-            <artifactId>opentelemetry-api</artifactId>
-            <optional>true</optional>
-        </dependency>
-        <dependency>
-            <groupId>io.opentelemetry.semconv</groupId>
-            <artifactId>opentelemetry-semconv</artifactId>
-            <optional>true</optional>
-        </dependency>
-        <!-- Dependencies used for integration with Micrometer extension -->
-        <dependency>
-            <groupId>io.micrometer</groupId>
-            <artifactId>micrometer-core</artifactId>
-            <optional>true</optional>
-        </dependency>
         <!-- Test dependencies -->
         <dependency>
             <groupId>org.junit.jupiter</groupId>
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/ErrorCountingInterceptor.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/ErrorCountingInterceptor.java
index f210470be07b0..dbad9843d69fa 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/ErrorCountingInterceptor.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/ErrorCountingInterceptor.java
@@ -1,17 +1,19 @@
 package io.quarkus.websockets.next.runtime.telemetry;
 
-import io.micrometer.core.instrument.Counter;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketMetricsInterceptorProducer.WebSocketMetricsInterceptor;
 
 final class ErrorCountingInterceptor implements ErrorInterceptor {
 
-    private final Counter errorCounter;
+    private final WebSocketMetricsInterceptor interceptor;
+    private final String path;
 
-    ErrorCountingInterceptor(Counter errorCounter) {
-        this.errorCounter = errorCounter;
+    ErrorCountingInterceptor(WebSocketMetricsInterceptor interceptor, String path) {
+        this.interceptor = interceptor;
+        this.path = path;
     }
 
     @Override
     public void intercept(Throwable throwable) {
-        errorCounter.increment();
+        interceptor.onError(path);
     }
 }
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsBuilderCustomizer.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsBuilderCustomizer.java
index f82186a0ceb12..ca8cd9dc5fef1 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsBuilderCustomizer.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsBuilderCustomizer.java
@@ -1,186 +1,101 @@
 package io.quarkus.websockets.next.runtime.telemetry;
 
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CLIENT_CONNECTION_CLOSED;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CLIENT_CONNECTION_OPENED;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CLIENT_CONNECTION_OPENED_ERROR;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CLIENT_ENDPOINT_COUNT_ERRORS;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.DIRECTION_TAG_KEY;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_CLOSED;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_ON_OPEN_ERROR;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_OPENED;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_ENDPOINT_COUNT_ERRORS;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.Direction.INBOUND;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.Direction.OUTBOUND;
-
 import java.util.function.Consumer;
 import java.util.function.Function;
 
-import io.micrometer.core.instrument.Counter;
-import io.micrometer.core.instrument.Meter;
-import io.micrometer.core.instrument.MeterRegistry;
+import jakarta.enterprise.inject.Instance;
+
 import io.quarkus.websockets.next.runtime.WebSocketEndpoint;
 import io.quarkus.websockets.next.runtime.config.WebSocketsClientRuntimeConfig;
 import io.quarkus.websockets.next.runtime.config.WebSocketsServerRuntimeConfig;
-import io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.Direction;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketMetricsInterceptorProducer;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketMetricsInterceptorProducer.WebSocketMetricsInterceptor;
 
 /**
  * Installs metrics support into the WebSockets extension.
  */
 public final class MetricsBuilderCustomizer implements Consumer<WebSocketTelemetryProviderBuilder> {
 
-    private static final String URI_TAG_KEY = "uri";
-
-    private final MeterRegistry meterRegistry;
+    private final Instance<WebSocketMetricsInterceptorProducer> interceptorProducerInstance;
     private final boolean serverMetricsEnabled;
     private final boolean clientMetricsEnabled;
 
-    MetricsBuilderCustomizer(MeterRegistry meterRegistry, WebSocketsServerRuntimeConfig serverRuntimeConfig,
-            WebSocketsClientRuntimeConfig clientRuntimeConfig) {
+    MetricsBuilderCustomizer(WebSocketsServerRuntimeConfig serverRuntimeConfig,
+            WebSocketsClientRuntimeConfig clientRuntimeConfig,
+            Instance<WebSocketMetricsInterceptorProducer> interceptorProducerInstance) {
         this.serverMetricsEnabled = serverRuntimeConfig.telemetry().metricsEnabled();
         this.clientMetricsEnabled = clientRuntimeConfig.telemetry().metricsEnabled();
-        this.meterRegistry = meterRegistry;
+        this.interceptorProducerInstance = interceptorProducerInstance;
     }
 
     @Override
     public void accept(WebSocketTelemetryProviderBuilder builder) {
-        if (clientMetricsEnabled) {
-            addClientMetricsSupport(builder);
-        }
-        if (serverMetricsEnabled) {
-            addServerMetricsSupport(builder);
+        if (interceptorProducerInstance.isResolvable()) {
+            final WebSocketMetricsInterceptorProducer interceptorProducer = interceptorProducerInstance.get();
+            if (clientMetricsEnabled) {
+                addClientMetricsSupport(builder, interceptorProducer);
+            }
+            if (serverMetricsEnabled) {
+                addServerMetricsSupport(builder, interceptorProducer);
+            }
         }
     }
 
-    private void addServerMetricsSupport(WebSocketTelemetryProviderBuilder builder) {
-        final Meter.MeterProvider<Counter> messagesCounter = Counter
-                .builder(TelemetryConstants.SERVER_COUNT)
-                .description("Number of messages sent and received by server endpoints.")
-                .withRegistry(meterRegistry);
-
-        final Meter.MeterProvider<Counter> bytesCounter = Counter
-                .builder(TelemetryConstants.SERVER_BYTES)
-                .description("Number of bytes sent and received by server endpoints.")
-                .withRegistry(meterRegistry);
-
+    private static void addServerMetricsSupport(WebSocketTelemetryProviderBuilder builder,
+            WebSocketMetricsInterceptorProducer interceptorProducer) {
+        final WebSocketMetricsInterceptor interceptor = interceptorProducer.createServerMetricsInterceptor();
         builder.serverEndpointDecorator(new Function<>() {
-
-            private final Meter.MeterProvider<Counter> closedConnectionCounter = Counter
-                    .builder(SERVER_CONNECTION_CLOSED)
-                    .description("Number of closed server WebSocket connections.")
-                    .withRegistry(meterRegistry);
-
             @Override
             public WebSocketEndpoint apply(TelemetryWebSocketEndpointContext ctx) {
-                return new MetricsForwardingWebSocketEndpoint(ctx.endpoint(),
-                        createCounter(messagesCounter, INBOUND, ctx.path()),
-                        createCounter(bytesCounter, INBOUND, ctx.path()),
-                        closedConnectionCounter.withTag(URI_TAG_KEY, ctx.path()));
+                return new MetricsForwardingWebSocketEndpoint(ctx.endpoint(), interceptor, ctx.path());
             }
         });
         builder.pathToServerErrorInterceptor(new Function<>() {
-
-            private final Meter.MeterProvider<Counter> serverErrorsCounter = Counter
-                    .builder(SERVER_ENDPOINT_COUNT_ERRORS)
-                    .description("Counts all the WebSockets server endpoint errors.")
-                    .withRegistry(meterRegistry);
-
             @Override
             public ErrorInterceptor apply(String path) {
-                return new ErrorCountingInterceptor(serverErrorsCounter.withTag(URI_TAG_KEY, path));
+                return new ErrorCountingInterceptor(interceptor, path);
+            }
+        });
+        builder.pathToServerSendingInterceptor(new Function<String, SendingInterceptor>() {
+            @Override
+            public SendingInterceptor apply(String path) {
+                return new MetricsSendingInterceptor(interceptor, path);
             }
         });
-        builder.pathToServerSendingInterceptor(createSendingInterceptor(bytesCounter, messagesCounter));
         builder.pathToServerConnectionInterceptor(new Function<>() {
-
-            private final Meter.MeterProvider<Counter> connectionOpenCounter = Counter
-                    .builder(SERVER_CONNECTION_OPENED)
-                    .description("Number of opened server connections.")
-                    .withRegistry(meterRegistry);
-
-            private final Meter.MeterProvider<Counter> connectionOpeningFailedCounter = Counter
-                    .builder(SERVER_CONNECTION_ON_OPEN_ERROR)
-                    .description("Number of failures occurred when opening server connection failed.")
-                    .withRegistry(meterRegistry);
-
             @Override
             public ConnectionInterceptor apply(String path) {
-                return new MetricsConnectionInterceptor(connectionOpenCounter.withTag(URI_TAG_KEY, path),
-                        connectionOpeningFailedCounter.withTag(URI_TAG_KEY, path));
+                return new MetricsConnectionInterceptor(interceptor, path);
             }
         });
     }
 
-    private void addClientMetricsSupport(WebSocketTelemetryProviderBuilder builder) {
-        final Meter.MeterProvider<Counter> messagesCounter = Counter
-                .builder(TelemetryConstants.CLIENT_COUNT)
-                .description("Number of messages sent and received by client endpoints.")
-                .withRegistry(meterRegistry);
-
-        final Meter.MeterProvider<Counter> bytesCounter = Counter
-                .builder(TelemetryConstants.CLIENT_BYTES)
-                .description("Number of bytes sent and received by client endpoints.")
-                .withRegistry(meterRegistry);
-
+    private static void addClientMetricsSupport(WebSocketTelemetryProviderBuilder builder,
+            WebSocketMetricsInterceptorProducer interceptorProducer) {
+        final WebSocketMetricsInterceptor interceptor = interceptorProducer.createClientMetricsInterceptor();
         builder.clientEndpointDecorator(new Function<>() {
-
-            private final Meter.MeterProvider<Counter> closedConnectionCounter = Counter
-                    .builder(CLIENT_CONNECTION_CLOSED)
-                    .description("Number of closed client WebSocket connections.")
-                    .withRegistry(meterRegistry);
-
             @Override
             public WebSocketEndpoint apply(TelemetryWebSocketEndpointContext ctx) {
-                return new MetricsForwardingWebSocketEndpoint(ctx.endpoint(),
-                        createCounter(messagesCounter, INBOUND, ctx.path()),
-                        createCounter(bytesCounter, INBOUND, ctx.path()),
-                        closedConnectionCounter.withTag(URI_TAG_KEY, ctx.path()));
+                return new MetricsForwardingWebSocketEndpoint(ctx.endpoint(), interceptor, ctx.path());
             }
         });
         builder.pathToClientErrorInterceptor(new Function<>() {
-
-            private final Meter.MeterProvider<Counter> clientErrorsCounter = Counter
-                    .builder(CLIENT_ENDPOINT_COUNT_ERRORS)
-                    .description("Counts all the WebSockets client endpoint errors.")
-                    .withRegistry(meterRegistry);
-
             @Override
             public ErrorInterceptor apply(String path) {
-                return new ErrorCountingInterceptor(clientErrorsCounter.withTag(URI_TAG_KEY, path));
+                return new ErrorCountingInterceptor(interceptor, path);
             }
         });
-        builder.pathToClientSendingInterceptor(createSendingInterceptor(bytesCounter, messagesCounter));
-        builder.pathToClientConnectionInterceptor(new Function<>() {
-
-            private final Meter.MeterProvider<Counter> connectionOpenCounter = Counter
-                    .builder(CLIENT_CONNECTION_OPENED)
-                    .description("Number of opened client connections.")
-                    .withRegistry(meterRegistry);
-
-            private final Meter.MeterProvider<Counter> connectionOpeningFailedCounter = Counter
-                    .builder(CLIENT_CONNECTION_OPENED_ERROR)
-                    .description("Number of failures occurred when opening client connection failed.")
-                    .withRegistry(meterRegistry);
-
+        builder.pathToClientSendingInterceptor(new Function<String, SendingInterceptor>() {
             @Override
-            public ConnectionInterceptor apply(String path) {
-                return new MetricsConnectionInterceptor(connectionOpenCounter.withTag(URI_TAG_KEY, path),
-                        connectionOpeningFailedCounter.withTag(URI_TAG_KEY, path));
+            public SendingInterceptor apply(String path) {
+                return new MetricsSendingInterceptor(interceptor, path);
             }
         });
-    }
-
-    private static Function<String, SendingInterceptor> createSendingInterceptor(Meter.MeterProvider<Counter> bytesCounter,
-            Meter.MeterProvider<Counter> messagesCounter) {
-        return new Function<String, SendingInterceptor>() {
+        builder.pathToClientConnectionInterceptor(new Function<>() {
             @Override
-            public SendingInterceptor apply(String path) {
-                return new MetricsSendingInterceptor(createCounter(messagesCounter, OUTBOUND, path),
-                        createCounter(bytesCounter, OUTBOUND, path));
+            public ConnectionInterceptor apply(String path) {
+                return new MetricsConnectionInterceptor(interceptor, path);
             }
-        };
-    }
-
-    private static Counter createCounter(Meter.MeterProvider<Counter> counter, Direction direction, String path) {
-        return counter.withTags(URI_TAG_KEY, path, DIRECTION_TAG_KEY, direction.toString());
+        });
     }
 }
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsConnectionInterceptor.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsConnectionInterceptor.java
index 263d491e39f0b..855d254248a95 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsConnectionInterceptor.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsConnectionInterceptor.java
@@ -2,26 +2,26 @@
 
 import java.util.Map;
 
-import io.micrometer.core.instrument.Counter;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketMetricsInterceptorProducer.WebSocketMetricsInterceptor;
 
 final class MetricsConnectionInterceptor implements ConnectionInterceptor {
 
-    private final Counter connectionOpenCounter;
-    private final Counter connectionOnOpenErrorsCounter;
+    private final WebSocketMetricsInterceptor interceptor;
+    private final String path;
 
-    MetricsConnectionInterceptor(Counter connectionOpenCounter, Counter connectionOpeninigFailedCounter) {
-        this.connectionOpenCounter = connectionOpenCounter;
-        this.connectionOnOpenErrorsCounter = connectionOpeninigFailedCounter;
+    MetricsConnectionInterceptor(WebSocketMetricsInterceptor interceptor, String path) {
+        this.interceptor = interceptor;
+        this.path = path;
     }
 
     @Override
     public void connectionOpened() {
-        connectionOpenCounter.increment();
+        interceptor.onConnectionOpened(path);
     }
 
     @Override
     public void connectionOpeningFailed(Throwable cause) {
-        connectionOnOpenErrorsCounter.increment();
+        interceptor.onConnectionOpeningFailed(path);
     }
 
     @Override
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsForwardingWebSocketEndpoint.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsForwardingWebSocketEndpoint.java
index 04094d8fb3805..33d1a2c38887c 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsForwardingWebSocketEndpoint.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsForwardingWebSocketEndpoint.java
@@ -1,22 +1,19 @@
 package io.quarkus.websockets.next.runtime.telemetry;
 
-import io.micrometer.core.instrument.Counter;
 import io.quarkus.websockets.next.runtime.WebSocketEndpoint;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketMetricsInterceptorProducer.WebSocketMetricsInterceptor;
 import io.vertx.core.Future;
 import io.vertx.core.buffer.Buffer;
 
 final class MetricsForwardingWebSocketEndpoint extends ForwardingWebSocketEndpoint {
 
-    private final Counter onMessageReceivedCounter;
-    private final Counter onMessageReceivedBytesCounter;
-    private final Counter onConnectionClosedCounter;
+    private final WebSocketMetricsInterceptor interceptor;
+    private final String path;
 
-    MetricsForwardingWebSocketEndpoint(WebSocketEndpoint delegate, Counter onMessageReceivedCounter,
-            Counter onMessageReceivedBytesCounter, Counter onConnectionClosedCounter) {
+    MetricsForwardingWebSocketEndpoint(WebSocketEndpoint delegate, WebSocketMetricsInterceptor interceptor, String path) {
         super(delegate);
-        this.onMessageReceivedCounter = onMessageReceivedCounter;
-        this.onMessageReceivedBytesCounter = onMessageReceivedBytesCounter;
-        this.onConnectionClosedCounter = onConnectionClosedCounter;
+        this.interceptor = interceptor;
+        this.path = path;
     }
 
     @Override
@@ -45,23 +42,19 @@ public Object decodeBinaryMultiItem(Object message) {
 
     @Override
     public Future<Void> onClose() {
-        onConnectionClosedCounter.increment();
+        interceptor.onConnectionClosed(path);
         return delegate.onClose();
     }
 
     private void addMetricsIfMessageIsString(Object message) {
         if (message instanceof String stringMessage) {
-            onMessageReceivedCounter.increment();
-            double bytesNum = stringMessage.getBytes().length;
-            onMessageReceivedBytesCounter.increment(bytesNum);
+            interceptor.onMessageReceived(stringMessage.getBytes(), path);
         }
     }
 
     private void addMetricsIfMessageIsBuffer(Object message) {
         if (message instanceof Buffer bufferMessage) {
-            onMessageReceivedCounter.increment();
-            double bytesNum = bufferMessage.getBytes().length;
-            onMessageReceivedBytesCounter.increment(bytesNum);
+            interceptor.onMessageReceived(bufferMessage.getBytes(), path);
         }
     }
 }
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsSendingInterceptor.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsSendingInterceptor.java
index b8841af91a027..b86612a8d0312 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsSendingInterceptor.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsSendingInterceptor.java
@@ -2,28 +2,26 @@
 
 import java.nio.charset.StandardCharsets;
 
-import io.micrometer.core.instrument.Counter;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketMetricsInterceptorProducer.WebSocketMetricsInterceptor;
 import io.vertx.core.buffer.Buffer;
 
 final class MetricsSendingInterceptor implements SendingInterceptor {
 
-    private final Counter onMessageSentCounter;
-    private final Counter onMessageSentBytesCounter;
+    private final WebSocketMetricsInterceptor interceptor;
+    private final String path;
 
-    MetricsSendingInterceptor(Counter onMessageSentCounter, Counter onMessageSentBytesCounter) {
-        this.onMessageSentCounter = onMessageSentCounter;
-        this.onMessageSentBytesCounter = onMessageSentBytesCounter;
+    MetricsSendingInterceptor(WebSocketMetricsInterceptor interceptor, String path) {
+        this.interceptor = interceptor;
+        this.path = path;
     }
 
     @Override
     public void onSend(String text) {
-        onMessageSentCounter.increment();
-        onMessageSentBytesCounter.increment(text.getBytes(StandardCharsets.UTF_8).length);
+        interceptor.onMessageSent(text.getBytes(StandardCharsets.UTF_8), path);
     }
 
     @Override
     public void onSend(Buffer message) {
-        onMessageSentCounter.increment();
-        onMessageSentBytesCounter.increment(message.getBytes().length);
+        interceptor.onMessageSent(message.getBytes(), path);
     }
 }
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryWebSocketEndpointContext.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryWebSocketEndpointContext.java
index cd472e7d55cfb..43a826b2a3fa1 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryWebSocketEndpointContext.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryWebSocketEndpointContext.java
@@ -2,12 +2,28 @@
 
 import java.util.Map;
 
+import io.quarkus.websockets.next.WebSocketClientConnection;
+import io.quarkus.websockets.next.WebSocketConnection;
 import io.quarkus.websockets.next.runtime.WebSocketConnectionBase;
 import io.quarkus.websockets.next.runtime.WebSocketEndpoint;
+import io.quarkus.websockets.next.runtime.spi.telemetry.EndpointKind;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketEndpointContext;
 
 /**
  * Data carrier used to instantiate {@link TelemetrySupport}.
  */
 record TelemetryWebSocketEndpointContext(WebSocketEndpoint endpoint, WebSocketConnectionBase connection, String path,
         Map<String, Object> contextData) {
+
+    WebSocketEndpointContext forClient() {
+        WebSocketClientConnection clientConnection = (WebSocketClientConnection) connection;
+        return new WebSocketEndpointContext(path, EndpointKind.CLIENT, contextData, connection.id(),
+                clientConnection.clientId());
+    }
+
+    WebSocketEndpointContext forServer() {
+        WebSocketConnection serverConnection = (WebSocketConnection) connection;
+        return new WebSocketEndpointContext(path, EndpointKind.SERVER, contextData, connection.id(),
+                serverConnection.endpointId());
+    }
 }
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesBuilderCustomizer.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesBuilderCustomizer.java
index 292df12befc17..5f6d790e713d4 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesBuilderCustomizer.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesBuilderCustomizer.java
@@ -1,21 +1,16 @@
 package io.quarkus.websockets.next.runtime.telemetry;
 
-import static io.quarkus.websockets.next.runtime.telemetry.TracesConnectionInterceptor.CONNECTION_OPENED_SPAN_CTX;
-
 import java.util.function.Consumer;
 import java.util.function.Function;
 
 import jakarta.enterprise.inject.Instance;
 import jakarta.inject.Inject;
 
-import io.opentelemetry.api.trace.SpanContext;
-import io.opentelemetry.api.trace.SpanKind;
-import io.opentelemetry.api.trace.Tracer;
-import io.quarkus.websockets.next.WebSocketClientConnection;
-import io.quarkus.websockets.next.WebSocketConnection;
 import io.quarkus.websockets.next.runtime.WebSocketEndpoint;
 import io.quarkus.websockets.next.runtime.config.WebSocketsClientRuntimeConfig;
 import io.quarkus.websockets.next.runtime.config.WebSocketsServerRuntimeConfig;
+import io.quarkus.websockets.next.runtime.spi.telemetry.EndpointKind;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketTracesInterceptor;
 
 /**
  * Installs traces support into the WebSockets extension.
@@ -29,54 +24,54 @@ public final class TracesBuilderCustomizer implements Consumer<WebSocketTelemetr
     WebSocketsClientRuntimeConfig clientRuntimeConfig;
 
     @Inject
-    Instance<Tracer> tracerInstance;
+    Instance<WebSocketTracesInterceptor> tracesInterceptorInstance;
 
     @Override
     public void accept(WebSocketTelemetryProviderBuilder builder) {
+        if (!tracesInterceptorInstance.isResolvable()) {
+            return;
+        }
         var serverTracesEnabled = serverRuntimeConfig.telemetry().tracesEnabled();
         var clientTracesEnabled = clientRuntimeConfig.telemetry().tracesEnabled();
         if (serverTracesEnabled || clientTracesEnabled) {
-            final Tracer tracer = tracerInstance.get();
+            final WebSocketTracesInterceptor tracesInterceptor = this.tracesInterceptorInstance.get();
             if (serverTracesEnabled) {
-                addServerTracesSupport(builder, tracer);
+                addServerTracesSupport(builder, tracesInterceptor);
             }
             if (clientTracesEnabled) {
-                addClientTracesSupport(builder, tracer);
+                addClientTracesSupport(builder, tracesInterceptor);
             }
         }
     }
 
-    private static void addServerTracesSupport(WebSocketTelemetryProviderBuilder builder, Tracer tracer) {
+    private static void addServerTracesSupport(WebSocketTelemetryProviderBuilder builder,
+            WebSocketTracesInterceptor tracesInterceptor) {
         builder.serverEndpointDecorator(new Function<>() {
             @Override
             public WebSocketEndpoint apply(TelemetryWebSocketEndpointContext ctx) {
-                var onOpenSpanCtx = (SpanContext) ctx.contextData().get(CONNECTION_OPENED_SPAN_CTX);
-                return new TracesForwardingWebSocketEndpoint(ctx.endpoint(), tracer, (WebSocketConnection) ctx.connection(),
-                        onOpenSpanCtx, ctx.path());
+                return new TracesForwardingWebSocketEndpoint(ctx.endpoint(), tracesInterceptor, ctx.forServer());
             }
         });
         builder.pathToServerConnectionInterceptor(new Function<>() {
             @Override
             public ConnectionInterceptor apply(String path) {
-                return new TracesConnectionInterceptor(tracer, SpanKind.SERVER, path);
+                return new TracesConnectionInterceptor(tracesInterceptor, path, EndpointKind.SERVER);
             }
         });
     }
 
-    private static void addClientTracesSupport(WebSocketTelemetryProviderBuilder builder, Tracer tracer) {
+    private static void addClientTracesSupport(WebSocketTelemetryProviderBuilder builder,
+            WebSocketTracesInterceptor tracesInterceptor) {
         builder.clientEndpointDecorator(new Function<>() {
             @Override
             public WebSocketEndpoint apply(TelemetryWebSocketEndpointContext ctx) {
-                var onOpenSpanCtx = (SpanContext) ctx.contextData().get(CONNECTION_OPENED_SPAN_CTX);
-                return new TracesForwardingWebSocketEndpoint(ctx.endpoint(), tracer,
-                        (WebSocketClientConnection) ctx.connection(),
-                        onOpenSpanCtx, ctx.path());
+                return new TracesForwardingWebSocketEndpoint(ctx.endpoint(), tracesInterceptor, ctx.forClient());
             }
         });
         builder.pathToClientConnectionInterceptor(new Function<>() {
             @Override
             public ConnectionInterceptor apply(String path) {
-                return new TracesConnectionInterceptor(tracer, SpanKind.CLIENT, path);
+                return new TracesConnectionInterceptor(tracesInterceptor, path, EndpointKind.CLIENT);
             }
         });
     }
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesConnectionInterceptor.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesConnectionInterceptor.java
index 9b01dde795aae..71b0c1108a1e3 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesConnectionInterceptor.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesConnectionInterceptor.java
@@ -1,65 +1,36 @@
 package io.quarkus.websockets.next.runtime.telemetry;
 
-import java.util.HashMap;
 import java.util.Map;
 
-import io.opentelemetry.api.trace.Span;
-import io.opentelemetry.api.trace.SpanContext;
-import io.opentelemetry.api.trace.SpanKind;
-import io.opentelemetry.api.trace.Tracer;
-import io.opentelemetry.semconv.UrlAttributes;
+import io.quarkus.websockets.next.runtime.spi.telemetry.EndpointKind;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketTracesInterceptor;
 
 final class TracesConnectionInterceptor implements ConnectionInterceptor {
 
-    static final String CONNECTION_OPENED_SPAN_CTX = "io.quarkus.websockets.next.connection-opened-span-ctx";
-
-    private final Tracer tracer;
+    private final WebSocketTracesInterceptor tracesInterceptor;
     private final String path;
-    private final Map<String, Object> contextData;
-    private final SpanKind spanKind;
+    private final EndpointKind endpointKind;
+    private volatile Map<String, Object> contextData;
 
-    TracesConnectionInterceptor(Tracer tracer, SpanKind spanKind, String path) {
-        this.tracer = tracer;
+    TracesConnectionInterceptor(WebSocketTracesInterceptor tracesInterceptor, String path, EndpointKind endpointKind) {
+        this.tracesInterceptor = tracesInterceptor;
         this.path = path;
-        this.contextData = new HashMap<>();
-        this.spanKind = spanKind;
+        this.endpointKind = endpointKind;
+        this.contextData = null;
     }
 
     @Override
     public void connectionOpened() {
-        var span = tracer.spanBuilder("OPEN " + path)
-                .setSpanKind(spanKind)
-                .addLink(previousSpanContext())
-                .setAttribute(UrlAttributes.URL_PATH, path)
-                .startSpan();
-        try (var ignored = span.makeCurrent()) {
-            contextData.put(CONNECTION_OPENED_SPAN_CTX, span.getSpanContext());
-        } finally {
-            span.end();
-        }
+        contextData = tracesInterceptor.onConnectionOpened(path, endpointKind);
     }
 
     @Override
     public void connectionOpeningFailed(Throwable cause) {
-        tracer.spanBuilder("OPEN " + path)
-                .setSpanKind(spanKind)
-                .addLink((SpanContext) contextData.get(CONNECTION_OPENED_SPAN_CTX))
-                .setAttribute(UrlAttributes.URL_PATH, path)
-                .startSpan()
-                .recordException(cause)
-                .end();
+        tracesInterceptor.onConnectionOpeningFailed(cause, path, endpointKind, contextData);
     }
 
     @Override
     public Map<String, Object> getContextData() {
-        return contextData;
-    }
-
-    private static SpanContext previousSpanContext() {
-        var span = Span.current();
-        if (span.getSpanContext().isValid()) {
-            return span.getSpanContext();
-        }
-        return null;
+        return contextData == null ? Map.of() : contextData;
     }
 }
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesForwardingWebSocketEndpoint.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesForwardingWebSocketEndpoint.java
index 6150d338f31e2..8c0819b56ddd2 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesForwardingWebSocketEndpoint.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesForwardingWebSocketEndpoint.java
@@ -1,18 +1,10 @@
 package io.quarkus.websockets.next.runtime.telemetry;
 
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CONNECTION_CLIENT_ATTR_KEY;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CONNECTION_ENDPOINT_ATTR_KEY;
-import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CONNECTION_ID_ATTR_KEY;
-
 import java.util.function.Function;
 
-import io.opentelemetry.api.trace.SpanContext;
-import io.opentelemetry.api.trace.SpanKind;
-import io.opentelemetry.api.trace.Tracer;
-import io.opentelemetry.semconv.UrlAttributes;
-import io.quarkus.websockets.next.WebSocketClientConnection;
-import io.quarkus.websockets.next.WebSocketConnection;
 import io.quarkus.websockets.next.runtime.WebSocketEndpoint;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketEndpointContext;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketTracesInterceptor;
 import io.vertx.core.Future;
 import io.vertx.core.Handler;
 
@@ -21,42 +13,14 @@
  */
 final class TracesForwardingWebSocketEndpoint extends ForwardingWebSocketEndpoint {
 
-    /**
-     * Target ID represents either endpoint id or client id.
-     */
-    private final String targetIdKey;
-    private final String targetIdValue;
-    /**
-     * Span context for an HTTP request used to establish the WebSocket connection.
-     */
-    private final Tracer tracer;
-    private final String connectionId;
-    private final String path;
-    private final SpanContext onOpenSpanContext;
-    private final SpanKind spanKind;
-
-    TracesForwardingWebSocketEndpoint(WebSocketEndpoint delegate, Tracer tracer, WebSocketConnection connection,
-            SpanContext onOpenSpanContext, String path) {
-        this(delegate, tracer, onOpenSpanContext, path, CONNECTION_ENDPOINT_ATTR_KEY, SpanKind.SERVER, connection.endpointId(),
-                connection.id());
-    }
-
-    TracesForwardingWebSocketEndpoint(WebSocketEndpoint delegate, Tracer tracer, WebSocketClientConnection connection,
-            SpanContext onOpenSpanContext, String path) {
-        this(delegate, tracer, onOpenSpanContext, path, CONNECTION_CLIENT_ATTR_KEY, SpanKind.CLIENT, connection.clientId(),
-                connection.id());
-    }
+    private final WebSocketTracesInterceptor tracesInterceptor;
+    private final WebSocketEndpointContext endpointContext;
 
-    private TracesForwardingWebSocketEndpoint(WebSocketEndpoint delegate, Tracer tracer, SpanContext onOpenSpanContext,
-            String path, String targetIdKey, SpanKind spanKind, String targetIdValue, String connectionId) {
+    TracesForwardingWebSocketEndpoint(WebSocketEndpoint delegate, WebSocketTracesInterceptor tracesInterceptor,
+            WebSocketEndpointContext endpointContext) {
         super(delegate);
-        this.tracer = tracer;
-        this.onOpenSpanContext = onOpenSpanContext;
-        this.connectionId = connectionId;
-        this.targetIdKey = targetIdKey;
-        this.targetIdValue = targetIdValue;
-        this.path = path;
-        this.spanKind = spanKind;
+        this.tracesInterceptor = tracesInterceptor;
+        this.endpointContext = endpointContext;
     }
 
     @Override
@@ -64,28 +28,13 @@ public Future<Void> onClose() {
         return delegate.onClose().map(new Function<Void, Void>() {
             @Override
             public Void apply(Void unused) {
-                tracer.spanBuilder("CLOSE " + path)
-                        .setSpanKind(spanKind)
-                        .addLink(onOpenSpanContext)
-                        .setAttribute(CONNECTION_ID_ATTR_KEY, connectionId)
-                        .setAttribute(UrlAttributes.URL_PATH, path)
-                        .setAttribute(targetIdKey, targetIdValue)
-                        .startSpan()
-                        .end();
+                tracesInterceptor.onConnectionClosed(endpointContext);
                 return null;
             }
         }).onFailure(new Handler<Throwable>() {
             @Override
             public void handle(Throwable throwable) {
-                tracer.spanBuilder("CLOSE " + path)
-                        .setSpanKind(spanKind)
-                        .addLink(onOpenSpanContext)
-                        .setAttribute(CONNECTION_ID_ATTR_KEY, connectionId)
-                        .setAttribute(UrlAttributes.URL_PATH, path)
-                        .setAttribute(targetIdKey, targetIdValue)
-                        .startSpan()
-                        .recordException(throwable)
-                        .end();
+                tracesInterceptor.onConnectionClosingFailed(throwable, endpointContext);
             }
         });
     }
diff --git a/extensions/websockets-next/spi/pom.xml b/extensions/websockets-next/spi/pom.xml
new file mode 100644
index 0000000000000..ea554c70b978c
--- /dev/null
+++ b/extensions/websockets-next/spi/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>quarkus-websockets-next-parent</artifactId>
+        <groupId>io.quarkus</groupId>
+        <version>999-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>quarkus-websockets-next-spi</artifactId>
+    <name>Quarkus - WebSockets Next - SPI</name>
+
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>default-compile</id>
+                        <configuration>
+                            <annotationProcessorPaths>
+                                <path>
+                                    <groupId>io.quarkus</groupId>
+                                    <artifactId>quarkus-extension-processor</artifactId>
+                                    <version>${project.version}</version>
+                                </path>
+                            </annotationProcessorPaths>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
\ No newline at end of file
diff --git a/extensions/websockets-next/spi/src/main/java/io/quarkus/websockets/next/runtime/spi/telemetry/EndpointKind.java b/extensions/websockets-next/spi/src/main/java/io/quarkus/websockets/next/runtime/spi/telemetry/EndpointKind.java
new file mode 100644
index 0000000000000..ba6e12374779a
--- /dev/null
+++ b/extensions/websockets-next/spi/src/main/java/io/quarkus/websockets/next/runtime/spi/telemetry/EndpointKind.java
@@ -0,0 +1,6 @@
+package io.quarkus.websockets.next.runtime.spi.telemetry;
+
+public enum EndpointKind {
+    SERVER,
+    CLIENT
+}
diff --git a/extensions/websockets-next/spi/src/main/java/io/quarkus/websockets/next/runtime/spi/telemetry/WebSocketEndpointContext.java b/extensions/websockets-next/spi/src/main/java/io/quarkus/websockets/next/runtime/spi/telemetry/WebSocketEndpointContext.java
new file mode 100644
index 0000000000000..4357e8b568ab9
--- /dev/null
+++ b/extensions/websockets-next/spi/src/main/java/io/quarkus/websockets/next/runtime/spi/telemetry/WebSocketEndpointContext.java
@@ -0,0 +1,16 @@
+package io.quarkus.websockets.next.runtime.spi.telemetry;
+
+import java.util.Map;
+
+/**
+ * A WebSocket endpoint contextual data needed when collecting WebSockets metrics and traces.
+ *
+ * @param route endpoint route
+ * @param endpointKind endpoint kind (either client endpoint or server endpoint)
+ * @param connectionContextStorage context data recorded when the WebSocket connection opened
+ * @param connectionId the WebSocket connection id
+ * @param targetId for a server endpoint, this is an endpoint id, while for a client endpoint, this is a client id
+ */
+public record WebSocketEndpointContext(String route, EndpointKind endpointKind, Map<String, Object> connectionContextStorage,
+        String connectionId, String targetId) {
+}
diff --git a/extensions/websockets-next/spi/src/main/java/io/quarkus/websockets/next/runtime/spi/telemetry/WebSocketMetricsInterceptorProducer.java b/extensions/websockets-next/spi/src/main/java/io/quarkus/websockets/next/runtime/spi/telemetry/WebSocketMetricsInterceptorProducer.java
new file mode 100644
index 0000000000000..5fd500908806d
--- /dev/null
+++ b/extensions/websockets-next/spi/src/main/java/io/quarkus/websockets/next/runtime/spi/telemetry/WebSocketMetricsInterceptorProducer.java
@@ -0,0 +1,69 @@
+package io.quarkus.websockets.next.runtime.spi.telemetry;
+
+/**
+ * This interface must be implemented as a CDI bean by an extension that needs to collect WebSockets metrics.
+ */
+public interface WebSocketMetricsInterceptorProducer {
+
+    /**
+     * Creates {@link WebSocketMetricsInterceptor} for server endpoints.
+     *
+     * @return WebSocketMetricsInterceptor
+     */
+    WebSocketMetricsInterceptor createServerMetricsInterceptor();
+
+    /**
+     * Creates {@link WebSocketMetricsInterceptor} for client endpoints.
+     *
+     * @return WebSocketMetricsInterceptor
+     */
+    WebSocketMetricsInterceptor createClientMetricsInterceptor();
+
+    interface WebSocketMetricsInterceptor {
+
+        /**
+         * Called when errors described by the 'OnError' annotation occur.
+         *
+         * @param route endpoint route
+         */
+        void onError(String route);
+
+        /**
+         * Called every time an outbound binary or text message is sent.
+         *
+         * @param data sent data
+         * @param route endpoint route
+         */
+        void onMessageSent(byte[] data, String route);
+
+        /**
+         * Called every time an inbound binary or text message is received.
+         *
+         * @param data received data
+         * @param route endpoint route
+         */
+        void onMessageReceived(byte[] data, String route);
+
+        /**
+         * Called when a WebSocket connection is opened.
+         *
+         * @param route endpoint route
+         */
+        void onConnectionOpened(String route);
+
+        /**
+         * Called when an opening of a WebSocket connection failed.
+         *
+         * @param route endpoint route
+         */
+        void onConnectionOpeningFailed(String route);
+
+        /**
+         * Called when a WebSocket connection is opened.
+         *
+         * @param route endpoint route
+         */
+        void onConnectionClosed(String route);
+
+    }
+}
diff --git a/extensions/websockets-next/spi/src/main/java/io/quarkus/websockets/next/runtime/spi/telemetry/WebSocketTracesInterceptor.java b/extensions/websockets-next/spi/src/main/java/io/quarkus/websockets/next/runtime/spi/telemetry/WebSocketTracesInterceptor.java
new file mode 100644
index 0000000000000..945bb18e2c360
--- /dev/null
+++ b/extensions/websockets-next/spi/src/main/java/io/quarkus/websockets/next/runtime/spi/telemetry/WebSocketTracesInterceptor.java
@@ -0,0 +1,47 @@
+package io.quarkus.websockets.next.runtime.spi.telemetry;
+
+import java.util.Map;
+
+/**
+ * This interface must be implemented as a CDI bean by an extension that needs to collect WebSockets traces.
+ */
+public interface WebSocketTracesInterceptor {
+
+    /**
+     * Called when a WebSocket connection is opened.
+     * Any data that should be shared between other events (like {@link #onConnectionClosed(WebSocketEndpointContext)})
+     * must be returned as a context data.
+     *
+     * @param route endpoint route
+     * @param endpointKind whether invoked endpoint is a server endpoint or a client endpoint
+     * @return context data or null (when no context data are required)
+     */
+    Map<String, Object> onConnectionOpened(String route, EndpointKind endpointKind);
+
+    /**
+     * Called when an opening of a WebSocket connection failed.
+     *
+     * @param cause {@link Throwable}
+     * @param route endpoint route
+     * @param endpointKind whether invoked endpoint is a server endpoint or a client endpoint
+     * @param connectionOpenedContext context data produced by the {@link #onConnectionOpened(String, EndpointKind)}
+     */
+    void onConnectionOpeningFailed(Throwable cause, String route, EndpointKind endpointKind,
+            Map<String, Object> connectionOpenedContext);
+
+    /**
+     * Called when a WebSocket connection is opened.
+     *
+     * @param context WebSocketEndpointContext
+     */
+    void onConnectionClosed(WebSocketEndpointContext context);
+
+    /**
+     * Called when a closing of a WebSocket connection failed.
+     *
+     * @param throwable {@link Throwable}
+     * @param context endpoint context
+     */
+    void onConnectionClosingFailed(Throwable throwable, WebSocketEndpointContext context);
+
+}


Reviews:
user: login: mkouba
body: Looks good but I cannot talk about the opentelemetry/micrometer part..
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-13T11:55:18Z
user: login: michalvavrik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-13T12:10:16Z
user: login: brunobat
body: I've decided this is good to go.
We will need to handle whatever happens after Observability.next, afterwards.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-31T16:09:12Z
user: login: cescoffier
body: Looks ok, however, we need to verify that:

1. This is not a breaking change
2. There are no split packages
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T05:58:20Z
user: login: cescoffier
body: My only concern is the URI tag in metrics. CC @brunobat 
It would be great to have the review from @mkouba 

state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-28T05:51:42Z
user: login: mkouba
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T06:46:46Z
user: login: mkouba
body: The WS Next part is ok. I just added a tiny nitpicking comment ;-).
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T06:50:40Z
user: login: brunobat
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-28T09:40:51Z
user: login: michalvavrik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T15:48:33Z
user: login: brunobat
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T15:37:19Z
user: login: michalvavrik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T15:41:41Z
user: login: michalvavrik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T11:50:30Z
user: login: brunobat
body: Thanks @michalvavrik 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-30T13:13:12Z
user: login: michalvavrik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T12:45:24Z
user: login: michalvavrik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T12:46:27Z
user: login: michalvavrik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T13:04:20Z

Review comments:
diff_hunk: @@ -237,6 +238,13 @@ void registerEndUserAttributesEventObserver(Capabilities capabilities,
         }
     }
 
+    @BuildStep
+    void collectWebSocketsNextTraces(BuildProducer<AdditionalBeanBuildItem> additionalBeanProducer, Capabilities capabilities) {
path: extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java
user: login: mkouba
body: ```suggestion
    void registerWebSocketTracesInterceptor(BuildProducer<AdditionalBeanBuildItem> additionalBeanProducer, Capabilities capabilities) {
```

Maybe?
created_at: 2025-01-13T11:42:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+package io.quarkus.micrometer.deployment.binder;
+
+import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
+import io.quarkus.deployment.Capabilities;
+import io.quarkus.deployment.Capability;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.BuildSteps;
+import io.quarkus.micrometer.deployment.MicrometerProcessor;
+import io.quarkus.micrometer.runtime.binder.websockets.WebSocketMetricsInterceptorProducerImpl;
+
+/**
+ * Add support for WebSockets Next instrumentation.
+ */
+@BuildSteps(onlyIf = MicrometerProcessor.MicrometerEnabled.class)
+public class WebSocketsBinderProcessor {
+
+    @BuildStep
+    void collectWebSocketsNextMetrics(BuildProducer<AdditionalBeanBuildItem> additionalBeanProducer,
path: extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/WebSocketsBinderProcessor.java
user: login: mkouba
body: ```suggestion
    void registerWebSocketMetricsInterceptor(BuildProducer<AdditionalBeanBuildItem> additionalBeanProducer,
```

Maybe?
created_at: 2025-01-13T11:47:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -237,6 +238,13 @@ void registerEndUserAttributesEventObserver(Capabilities capabilities,
         }
     }
 
+    @BuildStep
+    void collectWebSocketsNextTraces(BuildProducer<AdditionalBeanBuildItem> additionalBeanProducer, Capabilities capabilities) {
path: extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java
user: login: michalvavrik
body: sure thing
created_at: 2025-01-13T12:10:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,137 @@
+package io.quarkus.micrometer.runtime.binder.websockets;
+
+import static io.quarkus.micrometer.runtime.binder.websockets.WebSocketMetricConstants.DIRECTION_TAG_KEY;
+import static io.quarkus.micrometer.runtime.binder.websockets.WebSocketMetricConstants.Direction.INBOUND;
+import static io.quarkus.micrometer.runtime.binder.websockets.WebSocketMetricConstants.Direction.OUTBOUND;
+
+import jakarta.enterprise.context.Dependent;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketMetricsInterceptorProducer;
+
+@Dependent
+public final class WebSocketMetricsInterceptorProducerImpl implements WebSocketMetricsInterceptorProducer {
+
+    private static final String URI_TAG_KEY = "uri";
path: extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/websockets/WebSocketMetricsInterceptorProducerImpl.java
user: login: cescoffier
body: If we have path parameters, it can lead to an explosion. We need to apply the same approach as for HTTP.
created_at: 2025-04-28T05:47:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,7 @@
+package io.quarkus.websockets.next.runtime.spi.telemetry;
+
+import java.util.Map;
+
+public record WebSocketEndpointContext(String path, EndpointKind endpointKind, Map<String, Object> connectionContextStorage,
path: extensions/websockets-next/spi/src/main/java/io/quarkus/websockets/next/runtime/spi/telemetry/WebSocketEndpointContext.java
user: login: mkouba
body: It might make sense to add some javadoc for this record (given the fact that it's a part of a public SPI). 
created_at: 2025-04-28T06:46:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,12 +2,28 @@
 
 import java.util.Map;
 
+import io.quarkus.websockets.next.WebSocketClientConnection;
+import io.quarkus.websockets.next.WebSocketConnection;
 import io.quarkus.websockets.next.runtime.WebSocketConnectionBase;
 import io.quarkus.websockets.next.runtime.WebSocketEndpoint;
+import io.quarkus.websockets.next.runtime.spi.telemetry.EndpointKind;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketEndpointContext;
 
 /**
  * Data carrier used to instantiate {@link TelemetrySupport}.
  */
 record TelemetryWebSocketEndpointContext(WebSocketEndpoint endpoint, WebSocketConnectionBase connection, String path,
         Map<String, Object> contextData) {
+
+    WebSocketEndpointContext forClient() {
+        WebSocketClientConnection clientConnection = (WebSocketClientConnection) connection;
+        return new WebSocketEndpointContext(path, EndpointKind.CLIENT, contextData, connection.id(),
path: extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryWebSocketEndpointContext.java
user: login: brunobat
body: @cescoffier is right. This will cause cardinality explosion.
the `uri` must be the route and not the path. 
See: https://github.com/quarkusio/quarkus/blob/7c3c88df75702bd196f4ba3ea3ff2f3fd9807609/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/RestClientMetricsFilter.java#L71

The `UrlPathTemplate` property is used to set the route in the context of the request, when a particular lib doesn't provide it out of the box.

That header is set on many different frameworks. Here is an example for the resteasy client: https://github.com/quarkusio/quarkus/blob/9f918b5d8f4571e4b24e57234a86d5517c58895c/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/PathFeatureHandler.java#L17

And another one for the `quarkus-rest` server: 
https://github.com/quarkusio/quarkus/blob/5884c2b124b43166f84f1d7ecca2870af61673cd/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java#L113
created_at: 2025-04-28T09:40:33Z
author_association: MEMBER
diff_hunk: @@ -2,12 +2,28 @@
 
 import java.util.Map;
 
+import io.quarkus.websockets.next.WebSocketClientConnection;
+import io.quarkus.websockets.next.WebSocketConnection;
 import io.quarkus.websockets.next.runtime.WebSocketConnectionBase;
 import io.quarkus.websockets.next.runtime.WebSocketEndpoint;
+import io.quarkus.websockets.next.runtime.spi.telemetry.EndpointKind;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketEndpointContext;
 
 /**
  * Data carrier used to instantiate {@link TelemetrySupport}.
  */
 record TelemetryWebSocketEndpointContext(WebSocketEndpoint endpoint, WebSocketConnectionBase connection, String path,
         Map<String, Object> contextData) {
+
+    WebSocketEndpointContext forClient() {
+        WebSocketClientConnection clientConnection = (WebSocketClientConnection) connection;
+        return new WebSocketEndpointContext(path, EndpointKind.CLIENT, contextData, connection.id(),
path: extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryWebSocketEndpointContext.java
user: login: michalvavrik
body: You need metrics per WebSocket endpoint which is represented by the path (were we use template path variable). You only have that many endpoints. If you won't provide users information how many messages and data are transferred for one WebSocket endpoint, metrics become less useful.

Right now, both on the main branch and in this PR, here is a test that asserts code you are commenting on @brunobat :

- assertion: https://github.com/quarkusio/quarkus/blob/ecfd5855bd563a5e68dcc5668404f78150ad9eea/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MicrometerWebSocketsOnTextMessageTest.java#L139
- endpoint looks like this: https://github.com/quarkusio/quarkus/blob/ecfd5855bd563a5e68dcc5668404f78150ad9eea/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/ClientEndpointWithPathParams.java#L6

I don't think there is a cardinality explosion due to the path tag, but please give me an example, I may be missing something.
created_at: 2025-04-28T15:48:33Z
author_association: MEMBER
diff_hunk: @@ -2,12 +2,28 @@
 
 import java.util.Map;
 
+import io.quarkus.websockets.next.WebSocketClientConnection;
+import io.quarkus.websockets.next.WebSocketConnection;
 import io.quarkus.websockets.next.runtime.WebSocketConnectionBase;
 import io.quarkus.websockets.next.runtime.WebSocketEndpoint;
+import io.quarkus.websockets.next.runtime.spi.telemetry.EndpointKind;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketEndpointContext;
 
 /**
  * Data carrier used to instantiate {@link TelemetrySupport}.
  */
 record TelemetryWebSocketEndpointContext(WebSocketEndpoint endpoint, WebSocketConnectionBase connection, String path,
         Map<String, Object> contextData) {
+
+    WebSocketEndpointContext forClient() {
+        WebSocketClientConnection clientConnection = (WebSocketClientConnection) connection;
+        return new WebSocketEndpointContext(path, EndpointKind.CLIENT, contextData, connection.id(),
path: extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryWebSocketEndpointContext.java
user: login: brunobat
body: That's actually the `route` and the test is totally fine. 
I think we are having a communication misunderstanding because all method arguments and the annotation mention `path` and I could't figure out the code that resolves it, however, the test actually evaluates a route.
Route: `/client-endpoint-with-path-param/{name}`
Path: `/client-endpoint-with-path-param/123`

The problem is if the tags contain things like `/client-endpoint-with-path-param/123`
created_at: 2025-04-29T15:37:19Z
author_association: MEMBER
diff_hunk: @@ -2,12 +2,28 @@
 
 import java.util.Map;
 
+import io.quarkus.websockets.next.WebSocketClientConnection;
+import io.quarkus.websockets.next.WebSocketConnection;
 import io.quarkus.websockets.next.runtime.WebSocketConnectionBase;
 import io.quarkus.websockets.next.runtime.WebSocketEndpoint;
+import io.quarkus.websockets.next.runtime.spi.telemetry.EndpointKind;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketEndpointContext;
 
 /**
  * Data carrier used to instantiate {@link TelemetrySupport}.
  */
 record TelemetryWebSocketEndpointContext(WebSocketEndpoint endpoint, WebSocketConnectionBase connection, String path,
         Map<String, Object> contextData) {
+
+    WebSocketEndpointContext forClient() {
+        WebSocketClientConnection clientConnection = (WebSocketClientConnection) connection;
+        return new WebSocketEndpointContext(path, EndpointKind.CLIENT, contextData, connection.id(),
path: extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryWebSocketEndpointContext.java
user: login: michalvavrik
body: Ah, I didn't know what `route` is in this context @brunobat , ok, they are routes, not paths. I'll rename these arguments to avoid confusion. Thanks
created_at: 2025-04-29T15:41:41Z
author_association: MEMBER
diff_hunk: @@ -2,12 +2,28 @@
 
 import java.util.Map;
 
+import io.quarkus.websockets.next.WebSocketClientConnection;
+import io.quarkus.websockets.next.WebSocketConnection;
 import io.quarkus.websockets.next.runtime.WebSocketConnectionBase;
 import io.quarkus.websockets.next.runtime.WebSocketEndpoint;
+import io.quarkus.websockets.next.runtime.spi.telemetry.EndpointKind;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketEndpointContext;
 
 /**
  * Data carrier used to instantiate {@link TelemetrySupport}.
  */
 record TelemetryWebSocketEndpointContext(WebSocketEndpoint endpoint, WebSocketConnectionBase connection, String path,
         Map<String, Object> contextData) {
+
+    WebSocketEndpointContext forClient() {
+        WebSocketClientConnection clientConnection = (WebSocketClientConnection) connection;
+        return new WebSocketEndpointContext(path, EndpointKind.CLIENT, contextData, connection.id(),
path: extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryWebSocketEndpointContext.java
user: login: michalvavrik
body: Done. No pressure, but I am preparing one WS Next PR for a refresh token that will also need runtime SPI module, so I am looking forward to see this PR merged.
created_at: 2025-04-30T11:50:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,7 @@
+package io.quarkus.websockets.next.runtime.spi.telemetry;
+
+import java.util.Map;
+
+public record WebSocketEndpointContext(String path, EndpointKind endpointKind, Map<String, Object> connectionContextStorage,
path: extensions/websockets-next/spi/src/main/java/io/quarkus/websockets/next/runtime/spi/telemetry/WebSocketEndpointContext.java
user: login: michalvavrik
body: sorry @mkouba , completely missed this comment, I'll add some javadoc
created_at: 2025-05-01T12:45:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,137 @@
+package io.quarkus.micrometer.runtime.binder.websockets;
+
+import static io.quarkus.micrometer.runtime.binder.websockets.WebSocketMetricConstants.DIRECTION_TAG_KEY;
+import static io.quarkus.micrometer.runtime.binder.websockets.WebSocketMetricConstants.Direction.INBOUND;
+import static io.quarkus.micrometer.runtime.binder.websockets.WebSocketMetricConstants.Direction.OUTBOUND;
+
+import jakarta.enterprise.context.Dependent;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.quarkus.websockets.next.runtime.spi.telemetry.WebSocketMetricsInterceptorProducer;
+
+@Dependent
+public final class WebSocketMetricsInterceptorProducerImpl implements WebSocketMetricsInterceptorProducer {
+
+    private static final String URI_TAG_KEY = "uri";
path: extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/websockets/WebSocketMetricsInterceptorProducerImpl.java
user: login: michalvavrik
body: > If we have path parameters, it can lead to an explosion. We need to apply the same approach as for HTTP.

I answered this later in Bruno's thread, this is a route, not path parameter. I renamed arguments to make things clearer.
created_at: 2025-05-01T12:46:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,7 @@
+package io.quarkus.websockets.next.runtime.spi.telemetry;
+
+import java.util.Map;
+
+public record WebSocketEndpointContext(String path, EndpointKind endpointKind, Map<String, Object> connectionContextStorage,
path: extensions/websockets-next/spi/src/main/java/io/quarkus/websockets/next/runtime/spi/telemetry/WebSocketEndpointContext.java
user: login: michalvavrik
body: Done.
created_at: 2025-05-01T13:04:20Z
author_association: MEMBER

PR comments:
user: login: quarkus-bot[bot]
created_at: 2025-01-13T00:17:56Z
body: /cc @brunobat (micrometer,opentelemetry), @ebullient (micrometer), @radcortez (opentelemetry)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-01-13T00:50:16Z
body: 🎊 PR Preview dd7edc6845b2c251b7edb13f57d83ca2d45333ce has been successfully built and deployed to https://quarkus-pr-main-45522-preview.surge.sh/version/main/guides/

- Images of blog posts older than 3 months are not available.
- Newsletters older than 3 months are not available.

<img width="300" src="https://user-images.githubusercontent.com/507615/90250366-88233900-de6e-11ea-95a5-84f0762ffd39.png">


<!-- Preview status comment marker -->
author_association: NONE
user: login: quarkus-bot[bot]
created_at: 2025-01-13T00:51:49Z
body: ---
> :waning_crescent_moon: **_This workflow status is outdated as a new workflow run has been triggered._**
---

# Status for workflow `Quarkus Documentation CI`

This is the status report for running `Quarkus Documentation CI` on commit 139641c1e99550657dc1f07955bfc42970e7f9f4.



:white_check_mark: The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

> [!WARNING]
> There are other workflow runs running, you probably need to wait for their status before merging.

<!-- Build-Reporter/build-scans-check-run -->


<!-- Build-Reporter/msg-id:workflow-run-status-hidden:Quarkus Documentation CI -->
<!-- Build-Reporter/workflow-run-id:12738365888 -->
author_association: NONE
user: login: quarkus-bot[bot]
created_at: 2025-01-13T05:34:55Z
body: ---
> :waning_crescent_moon: **_This workflow status is outdated as a new workflow run has been triggered._**
---

# Status for workflow `Quarkus CI`

This is the status report for running `Quarkus CI` on commit 139641c1e99550657dc1f07955bfc42970e7f9f4.



:white_check_mark: The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

You can consult the [Develocity build scans](https://github.com/quarkusio/quarkus/runs/35507608507).


---

## Flaky tests - <a href="https://ge.quarkus.io/scans/tests">Develocity</a>

### :gear: JVM Tests - JDK 17

#### :package: extensions/smallrye-reactive-messaging/deployment

<p>✖ <code>io.quarkus.smallrye.reactivemessaging.hotreload.ConnectorChangeTest.testUpdatingConnector</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.smallrye.reactivemessaging.hotreload.ConnectorChangeTest&tests.test=testUpdatingConnector">History</a></p>

- `
Expecting actual:
  ["-6","-7","-9","-10","-11","-12","-13","-14"]
to start with:
  ["-6", "-7", "-8", "-9"]` - `java.lang.AssertionError`

<details>

```
java.lang.AssertionError: 

Expecting actual:
  ["-6","-7","-9","-10","-11","-12","-13","-14"]
to start with:
  ["-6", "-7", "-8", "-9"]

	at io.quarkus.smallrye.reactivemessaging.hotreload.ConnectorChangeTest.testUpdatingConnector(ConnectorChangeTest.java:41)
```

</details>



---

### :gear: JVM Tests - JDK 17 Windows

#### :package: integration-tests/opentelemetry-quickstart

<p>✖ <code>io.quarkus.it.opentelemetry.OpenTelemetryDisabledTest.buildTimeDisabled</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.it.opentelemetry.OpenTelemetryDisabledTest&tests.test=buildTimeDisabled">History</a></p>

- `Condition with Lambda expression in io.quarkus.it.opentelemetry.OpenTelemetryDisabledTest was not fulfilled within 200 milliseconds.` - `org.awaitility.core.ConditionTimeoutException`

<details>

```
org.awaitility.core.ConditionTimeoutException: Condition with Lambda expression in io.quarkus.it.opentelemetry.OpenTelemetryDisabledTest was not fulfilled within 200 milliseconds.
	at org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:167)
	at org.awaitility.core.CallableCondition.await(CallableCondition.java:78)
	at org.awaitility.core.CallableCondition.await(CallableCondition.java:26)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1006)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:975)
	at io.quarkus.it.opentelemetry.OpenTelemetryDisabledTest.buildTimeDisabled(OpenTelemetryDisabledTest.java:29)
	at java.base/java.lang.reflect.Method.invoke(Method.java:569)
```

</details>



<!-- Build-Reporter/msg-id:workflow-run-status-hidden:Quarkus CI -->
<!-- Build-Reporter/workflow-run-id:12738365891 -->
author_association: NONE
user: login: quarkus-bot[bot]
created_at: 2025-01-13T12:39:33Z
body: ---
> :waning_crescent_moon: **_This workflow status is outdated as a new workflow run has been triggered._**
---

# Status for workflow `Quarkus Documentation CI`

This is the status report for running `Quarkus Documentation CI` on commit 8d40ce18ad0d5b9204d92eef59d6122b07322ac8.



:white_check_mark: The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

> [!WARNING]
> There are other workflow runs running, you probably need to wait for their status before merging.

<!-- Build-Reporter/build-scans-check-run -->


<!-- Build-Reporter/msg-id:workflow-run-status-hidden:Quarkus Documentation CI -->
<!-- Build-Reporter/workflow-run-id:12746905454 -->
author_association: NONE
user: login: quarkus-bot[bot]
created_at: 2025-01-13T14:02:33Z
body: ---
> :waning_crescent_moon: **_This workflow status is outdated as a new workflow run has been triggered._**
---

# Status for workflow `Quarkus CI`

This is the status report for running `Quarkus CI` on commit 8d40ce18ad0d5b9204d92eef59d6122b07322ac8.



:white_check_mark: The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

You can consult the [Develocity build scans](https://github.com/quarkusio/quarkus/runs/35529984366).


---

## Flaky tests - <a href="https://ge.quarkus.io/scans/tests">Develocity</a>

### :gear: JVM Tests - JDK 17

#### :package: extensions/smallrye-reactive-messaging/deployment

<p>✖ <code>io.quarkus.smallrye.reactivemessaging.hotreload.ConnectorChangeTest.testUpdatingConnector</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.smallrye.reactivemessaging.hotreload.ConnectorChangeTest&tests.test=testUpdatingConnector">History</a></p>

- `
Expecting actual:
  ["-6","-7","-9","-10","-11","-12","-13","-14"]
to start with:
  ["-6", "-7", "-8", "-9"]` - `java.lang.AssertionError`

<details>

```
java.lang.AssertionError: 

Expecting actual:
  ["-6","-7","-9","-10","-11","-12","-13","-14"]
to start with:
  ["-6", "-7", "-8", "-9"]

	at io.quarkus.smallrye.reactivemessaging.hotreload.ConnectorChangeTest.testUpdatingConnector(ConnectorChangeTest.java:41)
```

</details>



---

### :gear: JVM Tests - JDK 17 Windows

#### :package: integration-tests/opentelemetry-quickstart

<p>✖ <code>io.quarkus.it.opentelemetry.OpenTelemetryDisabledTest.buildTimeDisabled</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.it.opentelemetry.OpenTelemetryDisabledTest&tests.test=buildTimeDisabled">History</a></p>

- `Condition with Lambda expression in io.quarkus.it.opentelemetry.OpenTelemetryDisabledTest was not fulfilled within 200 milliseconds.` - `org.awaitility.core.ConditionTimeoutException`

<details>

```
org.awaitility.core.ConditionTimeoutException: Condition with Lambda expression in io.quarkus.it.opentelemetry.OpenTelemetryDisabledTest was not fulfilled within 200 milliseconds.
	at org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:167)
	at org.awaitility.core.CallableCondition.await(CallableCondition.java:78)
	at org.awaitility.core.CallableCondition.await(CallableCondition.java:26)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1006)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:975)
	at io.quarkus.it.opentelemetry.OpenTelemetryDisabledTest.buildTimeDisabled(OpenTelemetryDisabledTest.java:29)
	at java.base/java.lang.reflect.Method.invoke(Method.java:569)
```

</details>



<!-- Build-Reporter/msg-id:workflow-run-status-hidden:Quarkus CI -->
<!-- Build-Reporter/workflow-run-id:12746905504 -->
author_association: NONE
user: login: gsmet
created_at: 2025-01-28T08:51:20Z
body: Hey @brunobat , could you have a look at this one? It looks like something we would want for the next LTS.
author_association: MEMBER
user: login: brunobat
created_at: 2025-01-28T09:35:34Z
body: I wish we had discussed this before implementing it because if we adopt the observation API in the future, we will need to rewrite it all again. Maybe we did, but I don't remember. Sorry.
Will review asap, but not today.
author_association: MEMBER
user: login: michalvavrik
created_at: 2025-01-28T10:01:30Z
body: > I wish we had discussed this before implementing it because if we adopt the observation API in the future, we will need to rewrite it all again. Maybe we did, but I don't remember. Sorry. Will review asap, but not today.

I don't mind rewriting it again @brunobat .
author_association: MEMBER
user: login: michalvavrik
created_at: 2025-01-28T10:03:24Z
body: > Maybe we did, but I don't remember.

no, we didn't, I didn't realize this could be improved, I am happy to meet you and rewrite this. Thanks
author_association: MEMBER
user: login: cescoffier
created_at: 2025-03-31T05:21:52Z
body: @brunobat any news?
author_association: MEMBER
user: login: gsmet
created_at: 2025-04-08T15:54:28Z
body: I rebased the PR to fix a conflict and get an updated CI run.
author_association: MEMBER
user: login: quarkus-bot[bot]
created_at: 2025-04-08T16:23:29Z
body: ---
> :waning_crescent_moon: **_This workflow status is outdated as a new workflow run has been triggered._**
---

# Status for workflow `Quarkus Documentation CI`

This is the status report for running `Quarkus Documentation CI` on commit 151ee9c139ca64794f07b14bf2f0a02c55ca0edb.



:white_check_mark: The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

> [!WARNING]
> There are other workflow runs running, you probably need to wait for their status before merging.

<!-- Build-Reporter/build-scans-check-run -->


<!-- Build-Reporter/msg-id:workflow-run-status-hidden:Quarkus Documentation CI -->
<!-- Build-Reporter/workflow-run-id:14338065284 -->
author_association: NONE
user: login: quarkus-bot[bot]
created_at: 2025-04-08T19:27:12Z
body: ---
> :waning_crescent_moon: **_This workflow status is outdated as a new workflow run has been triggered._**
---

# Status for workflow `Quarkus CI`

This is the status report for running `Quarkus CI` on commit 151ee9c139ca64794f07b14bf2f0a02c55ca0edb.



:white_check_mark: The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

You can consult the [Develocity build scans](https://github.com/quarkusio/quarkus/runs/40203064126).


---

## Flaky tests - <a href="https://ge.quarkus.io/scans/tests">Develocity</a>

### :gear: JVM Tests - JDK 17

#### :package: extensions/infinispan-cache/deployment

<p>✖ <code>io.quarkus.cache.infinispan.InfinispanCacheTest.testGetAsyncWithParallelCalls</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.cache.infinispan.InfinispanCacheTest&tests.test=testGetAsyncWithParallelCalls">History</a></p>

- `
expected: "thread1"
 but was: "thread2"` - `org.opentest4j.AssertionFailedError`

<details>

```
org.opentest4j.AssertionFailedError: 

expected: "thread1"
 but was: "thread2"
	at io.quarkus.cache.infinispan.InfinispanCacheTest.testGetAsyncWithParallelCalls(InfinispanCacheTest.java:283)
	at java.base/java.lang.reflect.Method.invoke(Method.java:569)
	at io.quarkus.test.QuarkusUnitTest.runExtensionMethod(QuarkusUnitTest.java:521)
	at io.quarkus.test.QuarkusUnitTest.interceptTestMethod(QuarkusUnitTest.java:435)
```

</details>



<!-- Build-Reporter/msg-id:workflow-run-status-hidden:Quarkus CI -->
<!-- Build-Reporter/workflow-run-id:14338065294 -->
author_association: NONE
user: login: michalvavrik
created_at: 2025-04-13T21:18:11Z
body: I'll check early next week.
author_association: MEMBER
user: login: michalvavrik
created_at: 2025-04-19T13:00:46Z
body: Sorry it took so long. I fight with time lately.

> Looks ok, however, we need to verify that:
> 
>     1. This is not a breaking change

This is not a breaking change.

>     2. There are no split packages

New classes are in `spi` package, so no split packages. Here https://github.com/quarkusio/quarkus/discussions/43130 you say that public API should be in `spi` package in SPI module. That is how this is done.

Could we merge this now, please?
author_association: MEMBER
user: login: michalvavrik
created_at: 2025-04-24T10:24:45Z
body: Resolved merge conflicts and rebased on current base.
author_association: MEMBER
user: login: quarkus-bot[bot]
created_at: 2025-04-24T11:00:26Z
body: ---
> :waning_crescent_moon: **_This workflow status is outdated as a new workflow run has been triggered._**
---

# Status for workflow `Quarkus Documentation CI`

This is the status report for running `Quarkus Documentation CI` on commit 1d7326a299ece2505a76fc8bef3cbec8f72ef9d6.



:white_check_mark: The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

> [!WARNING]
> There are other workflow runs running, you probably need to wait for their status before merging.

<!-- Build-Reporter/build-scans-check-run -->


<!-- Build-Reporter/msg-id:workflow-run-status-hidden:Quarkus Documentation CI -->
<!-- Build-Reporter/workflow-run-id:14639260915 -->
author_association: NONE
user: login: quarkus-bot[bot]
created_at: 2025-04-24T13:58:59Z
body: ---
> :waning_crescent_moon: **_This workflow status is outdated as a new workflow run has been triggered._**
---

# Status for workflow `Quarkus CI`

This is the status report for running `Quarkus CI` on commit 1d7326a299ece2505a76fc8bef3cbec8f72ef9d6.



## Failing Jobs



| Status | Name | Step | Failures | Logs | Raw logs | Build scan |
| :-:  | --  | --  | :-:  | :-:  | :-:  | :-:  |
| ✖ | JVM Tests - JDK 17 | `Build` | [Failures](#user-content-test-failures-job-41078157189) | [Logs](https://github.com/quarkusio/quarkus/actions/runs/14639260906/job/41078157189) | [Raw logs](https://github.com/quarkusio/quarkus/commit/1d7326a299ece2505a76fc8bef3cbec8f72ef9d6/checks/41078157189/logs) | [:mag:](https://ge.quarkus.io/s/lrctn74m476n6) |
| :heavy_check_mark: | JVM Tests - JDK 21 |  |  | [Logs](https://github.com/quarkusio/quarkus/actions/runs/14639260906/job/41078157548) | [Raw logs](https://github.com/quarkusio/quarkus/commit/1d7326a299ece2505a76fc8bef3cbec8f72ef9d6/checks/41078157548/logs) | [:mag:](https://ge.quarkus.io/s/ivqv2p3a5zoia) |
| ✖ | Native Tests - Observability | `Build` | [Failures](#user-content-test-failures-job-41078172364) | [Logs](https://github.com/quarkusio/quarkus/actions/runs/14639260906/job/41078172364) | [Raw logs](https://github.com/quarkusio/quarkus/commit/1d7326a299ece2505a76fc8bef3cbec8f72ef9d6/checks/41078172364/logs) | [:mag:](https://ge.quarkus.io/s/2hz7vamxdzm44) |

Full information is available in the [Build summary check run](https://github.com/quarkusio/quarkus/runs/41091633245).
You can consult the [Develocity build scans](https://github.com/quarkusio/quarkus/runs/41091555615).


## Failures

### :gear: JVM Tests - JDK 17 <a href="#user-content-test-failures-job-41078157189" id="test-failures-job-41078157189">#</a>

```diff
- Failing: devtools/cli 

```

#### :package: devtools/cli

<p>✖ <code>io.quarkus.cli.CliProjectGradleTest.</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.cli.CliProjectGradleTest&tests.test=">History</a> - <a href="https://github.com/quarkusio/quarkus/runs/41091633245#user-content-test-failure-io.quarkus.cli.cliprojectgradletest-1">More details</a> - <a href="https://github.com/quarkusio/quarkus/blob/1d7326a299ece2505a76fc8bef3cbec8f72ef9d6/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java">Source on GitHub</a></p>

<details>

```
java.lang.StackOverflowError
	at java.base/java.lang.System$2.encodeASCII(System.java:2431)
	at java.base/sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(UTF_8.java:456)
	at java.base/sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:564)
	at java.base/java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:585)
	at java.base/sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:293)
	at java.base/sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:282)
	at java.base/sun.nio.cs.StreamEncoder.write(StreamEncoder.java:132)
```

</details>

<p>✖ <code>io.quarkus.cli.CliProjectGradleTest.testCreateAppDefaults</code> line <code>120</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.cli.CliProjectGradleTest&tests.test=testCreateAppDefaults">History</a> - <a href="https://github.com/quarkusio/quarkus/runs/41091633245#user-content-test-failure-io.quarkus.cli.cliprojectgradletest-2">More details</a> - <a href="https://github.com/quarkusio/quarkus/blob/1d7326a299ece2505a76fc8bef3cbec8f72ef9d6/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java#L120">Source on GitHub</a></p>

<details>

```
org.opentest4j.AssertionFailedError: 
Expected OK return code. Result:
result: {
  exitCode: {1},
  system_err: {
FAILURE: Build failed with an exception.

* Where:
```

</details>

<p>✖ <code>io.quarkus.cli.CliProjectGradleTest.testExtensionList</code> line <code>228</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.cli.CliProjectGradleTest&tests.test=testExtensionList">History</a> - <a href="https://github.com/quarkusio/quarkus/runs/41091633245#user-content-test-failure-io.quarkus.cli.cliprojectgradletest-3">More details</a> - <a href="https://github.com/quarkusio/quarkus/blob/1d7326a299ece2505a76fc8bef3cbec8f72ef9d6/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java#L228">Source on GitHub</a></p>

<details>

```
org.opentest4j.AssertionFailedError: 
Expected OK return code. Result:
result: {
  exitCode: {1},
  system_err: {
FAILURE: Build failed with an exception.

* Where:
```

</details>


---

### :gear: Native Tests - Observability <a href="#user-content-test-failures-job-41078172364" id="test-failures-job-41078172364">#</a>

```diff
- Failing: integration-tests/opentelemetry-jdbc-instrumentation 

```

#### :package: integration-tests/opentelemetry-jdbc-instrumentation

<p>✖ <code>Failed to execute goal io.quarkus:quarkus-maven-plugin:999-SNAPSHOT:build (default) on project quarkus-integration-test-opentelemetry-jdbc-instrumentation: Failed to build quarkus application</code></p>



<!-- Build-Reporter/msg-id:workflow-run-status-hidden:Quarkus CI -->
<!-- Build-Reporter/workflow-run-id:14639260906 -->
author_association: NONE
user: login: brunobat
created_at: 2025-04-24T14:50:41Z
body: The JDBC error seems to be an oracle driver error:
```
Error: The heap snapshot verifier discovered a type not marked as reachable: AnalysisType<VMOption$Origin[] -> HotSpotType<[Lcom/sun/management/VMOption$Origin;, resolved>, allocated: false, inHeap: false, reachable: false>
com.oracle.graal.pointsto.util.AnalysisError: The heap snapshot verifier discovered a type not marked as reachable: AnalysisType<VMOption$Origin[] -> HotSpotType<[Lcom/sun/management/VMOption$Origin;, resolved>, allocated: false, inHeap: false, reachable: false>
```
Not sure why this is happening. Neither Mandrel, the Oracle image or the Oracle driver seem to have changed. Mem issues?
author_association: MEMBER
user: login: michalvavrik
created_at: 2025-04-24T15:19:18Z
body: > rifier discovered a type not marked as reachable: AnalysisType<VMOption$Origin[] -> HotSpotType<[Lcom/sun/management/VMOption$Origin;, resolved>, allocated: false, inHeap: false, reachable: false>

Any chance it is this one https://github.com/quarkusio/quarkus/issues/46890 ?
author_association: MEMBER
user: login: brunobat
created_at: 2025-04-24T15:52:44Z
body: > > rifier discovered a type not marked as reachable: AnalysisType<VMOption$Origin[] -> HotSpotType<[Lcom/sun/management/VMOption$Origin;, resolved>, allocated: false, inHeap: false, reachable: false>
> 
> Any chance it is this one #46890 ?

Makes sense, but I'm intrigued why we haven't seen this failure sooner...
author_association: MEMBER
user: login: quarkus-bot[bot]
created_at: 2025-04-27T15:20:35Z
body: ---
> :waning_crescent_moon: **_This workflow status is outdated as a new workflow run has been triggered._**
---

# Status for workflow `Quarkus Documentation CI`

This is the status report for running `Quarkus Documentation CI` on commit 4f52cf2f5e18ad14809d232b8a4273f774659757.



:white_check_mark: The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

> [!WARNING]
> There are other workflow runs running, you probably need to wait for their status before merging.

<!-- Build-Reporter/build-scans-check-run -->


<!-- Build-Reporter/msg-id:workflow-run-status-hidden:Quarkus Documentation CI -->
<!-- Build-Reporter/workflow-run-id:14693183750 -->
author_association: NONE
user: login: quarkus-bot[bot]
created_at: 2025-04-27T18:06:19Z
body: ---
> :waning_crescent_moon: **_This workflow status is outdated as a new workflow run has been triggered._**
---

# Status for workflow `Quarkus CI`

This is the status report for running `Quarkus CI` on commit 4f52cf2f5e18ad14809d232b8a4273f774659757.



:white_check_mark: The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

You can consult the [Develocity build scans](https://github.com/quarkusio/quarkus/runs/41234835886).


---

## Flaky tests - <a href="https://ge.quarkus.io/scans/tests">Develocity</a>

### :gear: JVM Tests - JDK 17

#### :package: extensions/smallrye-reactive-messaging/deployment

<p>✖ <code>io.quarkus.smallrye.reactivemessaging.hotreload.ConnectorChangeTest.testUpdatingConnector</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.smallrye.reactivemessaging.hotreload.ConnectorChangeTest&tests.test=testUpdatingConnector">History</a></p>

- `
Expecting actual:
  ["-6","-7","-8","-10","-11","-12","-13","-14"]
to start with:
  ["-6", "-7", "-8", "-9"]` - `java.lang.AssertionError`

<details>

```
java.lang.AssertionError: 

Expecting actual:
  ["-6","-7","-8","-10","-11","-12","-13","-14"]
to start with:
  ["-6", "-7", "-8", "-9"]

	at io.quarkus.smallrye.reactivemessaging.hotreload.ConnectorChangeTest.testUpdatingConnector(ConnectorChangeTest.java:41)
```

</details>



---

### :gear: JVM Tests - JDK 17 Windows

#### :package: extensions/websockets-next/deployment

<p>✖ <code>io.quarkus.websockets.next.test.telemetry.MicrometerWebSocketsOnErrorTest.testClientEndpointError_ExceptionInsideOnTextMessage_WithOverloadedOnError</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.websockets.next.test.telemetry.MicrometerWebSocketsOnErrorTest&tests.test=testClientEndpointError_ExceptionInsideOnTextMessage_WithOverloadedOnError">History</a></p>

- `Assertion condition defined as a Lambda expression in io.quarkus.websockets.next.test.telemetry.MetricsAsserter 1 expectation failed.
Response body doesn't match expectation.
Expected: Key 'quarkus_websockets_server_endpoint_count_errors_total' with value '4' and direction 'null'
  Actual: \# TYPE http_server_requests_seconds summary
\# HELP http_server_requests_seconds HTTP server request processing time` - `org.awaitility.core.ConditionTimeoutException`

<details>

```
org.awaitility.core.ConditionTimeoutException: 
Assertion condition defined as a Lambda expression in io.quarkus.websockets.next.test.telemetry.MetricsAsserter 1 expectation failed.
Response body doesn't match expectation.
Expected: Key 'quarkus_websockets_server_endpoint_count_errors_total' with value '4' and direction 'null'
  Actual: # TYPE http_server_requests_seconds summary
# HELP http_server_requests_seconds HTTP server request processing time
http_server_requests_seconds_count{method="GET",outcome="INFORMATIONAL",status="101",uri="/client-error-no-on-error"} 1.0
http_server_requests_seconds_sum{method="GET",outcome="INFORMATIONAL",status="101",uri="/client-error-no-on-error"} 0.0012542
```

</details>


<p>✖ <code>io.quarkus.websockets.next.test.telemetry.MicrometerWebSocketsOnErrorTest.testClientEndpointError_ExceptionInsideOnTextMessage_noExplicitOnErrorDefined</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.websockets.next.test.telemetry.MicrometerWebSocketsOnErrorTest&tests.test=testClientEndpointError_ExceptionInsideOnTextMessage_noExplicitOnErrorDefined">History</a></p>

- `Assertion condition defined as a Lambda expression in io.quarkus.websockets.next.test.telemetry.MetricsAsserter 1 expectation failed.
Response body doesn't match expectation.
Expected: Key 'quarkus_websockets_server_endpoint_count_errors_total' with value '3' and direction 'null'
  Actual: \# TYPE http_server_requests_seconds summary
\# HELP http_server_requests_seconds HTTP server request processing time` - `org.awaitility.core.ConditionTimeoutException`

<details>

```
org.awaitility.core.ConditionTimeoutException: 
Assertion condition defined as a Lambda expression in io.quarkus.websockets.next.test.telemetry.MetricsAsserter 1 expectation failed.
Response body doesn't match expectation.
Expected: Key 'quarkus_websockets_server_endpoint_count_errors_total' with value '3' and direction 'null'
  Actual: # TYPE http_server_requests_seconds summary
# HELP http_server_requests_seconds HTTP server request processing time
http_server_requests_seconds_count{method="GET",outcome="INFORMATIONAL",status="101",uri="/client-error-no-on-error"} 1.0
http_server_requests_seconds_sum{method="GET",outcome="INFORMATIONAL",status="101",uri="/client-error-no-on-error"} 0.0012542
```

</details>


<p>✖ <code>io.quarkus.websockets.next.test.telemetry.MicrometerWebSocketsOnErrorTest.testServerEndpointError_ExceptionInsideOnOpen</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.websockets.next.test.telemetry.MicrometerWebSocketsOnErrorTest&tests.test=testServerEndpointError_ExceptionInsideOnOpen">History</a></p>

- `io.vertx.core.impl.NoStackTraceThrowable: WebSocket is closed` - `java.util.concurrent.CompletionException`

<details>

```
java.util.concurrent.CompletionException: io.vertx.core.impl.NoStackTraceThrowable: WebSocket is closed
	at java.base/java.util.concurrent.CompletableFuture.reportJoin(CompletableFuture.java:413)
	at java.base/java.util.concurrent.CompletableFuture.join(CompletableFuture.java:2118)
	at io.quarkus.websockets.next.test.utils.WSClient.await(WSClient.java:137)
	at io.quarkus.websockets.next.test.utils.WSClient.sendAndAwait(WSClient.java:96)
	at io.quarkus.websockets.next.test.telemetry.Connection.openConnectionThenSend(Connection.java:34)
	at io.quarkus.websockets.next.test.telemetry.MicrometerWebSocketsOnErrorTest.testServerEndpointError_ExceptionInsideOnOpen(MicrometerWebSocketsOnErrorTest.java:201)
	at java.base/java.lang.reflect.Method.invoke(Method.java:569)
```

</details>


<p>✖ <code>io.quarkus.websockets.next.test.telemetry.MicrometerWebSocketsOnErrorTest.testServerEndpointError_GlobalErrorHandler</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.websockets.next.test.telemetry.MicrometerWebSocketsOnErrorTest&tests.test=testServerEndpointError_GlobalErrorHandler">History</a></p>

- `Assertion condition defined as a Lambda expression in io.quarkus.websockets.next.test.telemetry.MetricsAsserter 1 expectation failed.
Response body doesn't match expectation.
Expected: Key 'quarkus_websockets_server_endpoint_count_errors_total' with value '4' and direction 'null'
  Actual: \# TYPE http_server_requests_seconds summary
\# HELP http_server_requests_seconds HTTP server request processing time` - `org.awaitility.core.ConditionTimeoutException`

<details>

```
org.awaitility.core.ConditionTimeoutException: 
Assertion condition defined as a Lambda expression in io.quarkus.websockets.next.test.telemetry.MetricsAsserter 1 expectation failed.
Response body doesn't match expectation.
Expected: Key 'quarkus_websockets_server_endpoint_count_errors_total' with value '4' and direction 'null'
  Actual: # TYPE http_server_requests_seconds summary
# HELP http_server_requests_seconds HTTP server request processing time
http_server_requests_seconds_count{method="GET",outcome="INFORMATIONAL",status="101",uri="/client-error-no-on-error"} 1.0
http_server_requests_seconds_sum{method="GET",outcome="INFORMATIONAL",status="101",uri="/client-error-no-on-error"} 0.0012542
```

</details>



<!-- Build-Reporter/msg-id:workflow-run-status-hidden:Quarkus CI -->
<!-- Build-Reporter/workflow-run-id:14693183751 -->
author_association: NONE
user: login: michalvavrik
created_at: 2025-04-27T18:56:15Z
body: The failure is gone, `MicrometerWebSocketsOnErrorTest` seems flaky on Windows, but that won't be due to this PR (as it changes nothing functionally).
author_association: MEMBER
user: login: michalvavrik
created_at: 2025-04-27T18:57:11Z
body: I have requested 2 other reviews because it's unclear to me what is current situation (what are we waiting for with merging).
author_association: MEMBER
user: login: quarkus-bot[bot]
created_at: 2025-04-30T12:19:01Z
body: ---
> :waning_crescent_moon: **_This workflow status is outdated as a new workflow run has been triggered._**
---

# Status for workflow `Quarkus Documentation CI`

This is the status report for running `Quarkus Documentation CI` on commit 1e3128af396bea34ae7da7984f21e1a140935c13.



:white_check_mark: The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

> [!WARNING]
> There are other workflow runs running, you probably need to wait for their status before merging.

<!-- Build-Reporter/build-scans-check-run -->


<!-- Build-Reporter/msg-id:workflow-run-status-hidden:Quarkus Documentation CI -->
<!-- Build-Reporter/workflow-run-id:14753739286 -->
author_association: NONE
user: login: quarkus-bot[bot]
created_at: 2025-04-30T15:17:44Z
body: ---
> :waning_crescent_moon: **_This workflow status is outdated as a new workflow run has been triggered._**
---

# Status for workflow `Quarkus CI`

This is the status report for running `Quarkus CI` on commit 1e3128af396bea34ae7da7984f21e1a140935c13.



:white_check_mark: The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

You can consult the [Develocity build scans](https://github.com/quarkusio/quarkus/runs/41431475970).


---

## Flaky tests - <a href="https://ge.quarkus.io/scans/tests">Develocity</a>

### :gear: JVM Tests - JDK 17

#### :package: core/deployment

<p>✖ <code>io.quarkus.deployment.dev.FileSystemWatcherTestCase.testFileSystemWatcher</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.deployment.dev.FileSystemWatcherTestCase&tests.test=testFileSystemWatcher">History</a></p>

- `expected: not <null>` - `org.opentest4j.AssertionFailedError`

<details>

```
org.opentest4j.AssertionFailedError: expected: not <null>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:152)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertNotNull.failNull(AssertNotNull.java:49)
	at org.junit.jupiter.api.AssertNotNull.assertNotNull(AssertNotNull.java:35)
	at org.junit.jupiter.api.AssertNotNull.assertNotNull(AssertNotNull.java:30)
	at org.junit.jupiter.api.Assertions.assertNotNull(Assertions.java:304)
	at io.quarkus.deployment.dev.FileSystemWatcherTestCase.consumeEvents(FileSystemWatcherTestCase.java:169)
```

</details>



---

### :gear: JVM Tests - JDK 17 Windows

#### :package: extensions/micrometer-opentelemetry/deployment

<p>✖ <code>io.quarkus.micrometer.opentelemetry.deployment.compatibility.MicrometerTimedInterceptorTest.testTimeMethod</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.micrometer.opentelemetry.deployment.compatibility.MicrometerTimedInterceptorTest&tests.test=testTimeMethod">History</a></p>

- `Stream has no elements` - `java.lang.IllegalArgumentException`

<details>

```
java.lang.IllegalArgumentException: Stream has no elements
	at io.quarkus.micrometer.opentelemetry.deployment.common.MetricDataFilter.lambda$lastReading$2(MetricDataFilter.java:213)
	at java.base/java.util.Optional.orElseThrow(Optional.java:403)
	at io.quarkus.micrometer.opentelemetry.deployment.common.MetricDataFilter.lastReading(MetricDataFilter.java:213)
	at io.quarkus.micrometer.opentelemetry.deployment.common.MetricDataFilter.lastReadingDataPoint(MetricDataFilter.java:231)
	at io.quarkus.micrometer.opentelemetry.deployment.compatibility.MicrometerTimedInterceptorTest.testTimeMethod(MicrometerTimedInterceptorTest.java:77)
	at java.base/java.lang.reflect.Method.invoke(Method.java:569)
	at io.quarkus.test.QuarkusUnitTest.runExtensionMethod(QuarkusUnitTest.java:521)
```

</details>



---

### :gear: JVM Integration Tests - JDK 21

#### :package: integration-tests/opentelemetry-grpc-only

<p>✖ <code>io.quarkus.it.opentelemetry.grpc.HelloGrpcClientTest.testHello</code> - <a href="https://ge.quarkus.io/scans/tests?tests.container=io.quarkus.it.opentelemetry.grpc.HelloGrpcClientTest&tests.test=testHello">History</a></p>

- `java.lang.RuntimeException: Failed to start quarkus` - `java.lang.RuntimeException`

<details>

```
java.lang.RuntimeException: java.lang.RuntimeException: Failed to start quarkus
	at io.quarkus.test.junit.QuarkusTestExtension.throwBootFailureException(QuarkusTestExtension.java:693)
	at io.quarkus.test.junit.QuarkusTestExtension.interceptTestClassConstructor(QuarkusTestExtension.java:791)
	at java.base/java.util.Optional.orElseGet(Optional.java:364)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.lang.RuntimeException: Failed to start quarkus
	at io.quarkus.runner.ApplicationImpl.doStart(Unknown Source)
```

</details>



<!-- Build-Reporter/msg-id:workflow-run-status-hidden:Quarkus CI -->
<!-- Build-Reporter/workflow-run-id:14753739281 -->
author_association: NONE
user: login: michalvavrik
created_at: 2025-05-02T07:22:34Z
body: GH CI stuck on "CI Sanity Check" (Expected - Waiting for status to be reported). I'll rebase again.
author_association: MEMBER
user: login: quarkus-bot[bot]
created_at: 2025-05-02T08:01:35Z
body: # Status for workflow `Quarkus Documentation CI`

This is the status report for running `Quarkus Documentation CI` on commit ca75c710de9ca3dd38ff21c48e54dd7f83c75136.



:white_check_mark: The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

> [!WARNING]
> There are other workflow runs running, you probably need to wait for their status before merging.

<!-- Build-Reporter/build-scans-check-run -->


<!-- Build-Reporter/msg-id:workflow-run-status-active:Quarkus Documentation CI -->
<!-- Build-Reporter/workflow-run-id:14790743753 -->
author_association: NONE
user: login: quarkus-bot[bot]
created_at: 2025-05-02T10:41:09Z
body: # Status for workflow `Quarkus CI`

This is the status report for running `Quarkus CI` on commit ca75c710de9ca3dd38ff21c48e54dd7f83c75136.



:white_check_mark: The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

You can consult the [Develocity build scans](https://github.com/quarkusio/quarkus/runs/41535931666).


<!-- Build-Reporter/msg-id:workflow-run-status-active:Quarkus CI -->
<!-- Build-Reporter/workflow-run-id:14790743755 -->
author_association: NONE