diff --git a/docs/src/main/asciidoc/opentelemetry-tracing.adoc b/docs/src/main/asciidoc/opentelemetry-tracing.adoc
index ef62e35cbb4039..0b83a9587e61d8 100644
--- a/docs/src/main/asciidoc/opentelemetry-tracing.adoc
+++ b/docs/src/main/asciidoc/opentelemetry-tracing.adoc
@@ -573,6 +573,7 @@ See the main xref:opentelemetry.adoc#exporters[OpenTelemetry Guide exporters] se
** Kafka
** Pulsar
* https://quarkus.io/guides/vertx[`quarkus-vertx`] (http requests)
+* xref:websockets-next-reference.adoc[`websockets-next`]
=== Disable parts of the automatic tracing
diff --git a/docs/src/main/asciidoc/websockets-next-reference.adoc b/docs/src/main/asciidoc/websockets-next-reference.adoc
index 3d78f97ba32476..7935bb99743581 100644
--- a/docs/src/main/asciidoc/websockets-next-reference.adoc
+++ b/docs/src/main/asciidoc/websockets-next-reference.adoc
@@ -1131,6 +1131,19 @@ quarkus.log.category."io.quarkus.websockets.next.traffic".level=DEBUG <3>
<2> Set the number of characters of a text message payload which will be logged.
<3> Enable `DEBUG` level is for the logger `io.quarkus.websockets.next.traffic`.
+[[telemetry]]
+== Telemetry
+
+When the OpenTelemetry extension is present, traces for opened and closed WebSocket connections are collected by default.
+If you do not require WebSocket traces, you can disable collecting of traces like in the example below:
+
+[source, properties]
+----
+quarkus.websockets-next.server.traces.enabled=false
+quarkus.websockets-next.client.traces.enabled=false
+----
+
+NOTE: Telemetry for the `BasicWebSocketConnector` is currently not supported.
[[websocket-next-configuration-reference]]
== Configuration reference
diff --git a/extensions/websockets-next/deployment/pom.xml b/extensions/websockets-next/deployment/pom.xml
index 7681fcf852e7bd..c6b47704d0f510 100644
--- a/extensions/websockets-next/deployment/pom.xml
+++ b/extensions/websockets-next/deployment/pom.xml
@@ -80,6 +80,17 @@
mutiny-kotlin
test
+
+
+ io.opentelemetry
+ opentelemetry-sdk-testing
+ test
+
+
+ io.opentelemetry.semconv
+ opentelemetry-semconv
+ test
+
diff --git a/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java b/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java
index bb2f699544ce44..aced79ae39472b 100644
--- a/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java
+++ b/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java
@@ -20,7 +20,9 @@
import java.util.stream.Collectors;
import jakarta.enterprise.context.SessionScoped;
+import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.invoke.Invoker;
+import jakarta.inject.Singleton;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTransformation;
@@ -31,6 +33,7 @@
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
+import org.jboss.jandex.ParameterizedType;
import org.jboss.jandex.PrimitiveType;
import org.jboss.jandex.Type;
import org.jboss.jandex.Type.Kind;
@@ -123,6 +126,10 @@
import io.quarkus.websockets.next.runtime.WebSocketSessionContext;
import io.quarkus.websockets.next.runtime.kotlin.ApplicationCoroutineScope;
import io.quarkus.websockets.next.runtime.kotlin.CoroutineInvoker;
+import io.quarkus.websockets.next.runtime.telemetry.TracesBuilderCustomizer;
+import io.quarkus.websockets.next.runtime.telemetry.WebSocketTelemetryProvider;
+import io.quarkus.websockets.next.runtime.telemetry.WebSocketTelemetryProviderBuilder;
+import io.quarkus.websockets.next.runtime.telemetry.WebSocketTelemetryRecorder;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.groups.UniCreate;
@@ -459,7 +466,8 @@ public void registerRoutes(WebSocketServerRecorder recorder, List additionalBeanProducer) {
+ if (isTracesSupportEnabled(capabilities)) {
+ additionalBeanProducer.produce(AdditionalBeanBuildItem.unremovableOf(TracesBuilderCustomizer.class));
+ }
+ }
+
+ @BuildStep
+ @Record(RUNTIME_INIT)
+ void createTelemetryProvider(BuildProducer syntheticBeanProducer,
+ WebSocketTelemetryRecorder recorder, Capabilities capabilities) {
+ if (isTracesSupportEnabled(capabilities)) {
+ var syntheticBeanBuildItem = SyntheticBeanBuildItem
+ .configure(WebSocketTelemetryProvider.class)
+ .setRuntimeInit() // consumes runtime config: traces / metrics enabled
+ .unremovable()
+ // inject point type: List>
+ .addInjectionPoint(
+ ParameterizedType.create(
+ DotName.createSimple(Instance.class),
+ new Type[] { ParameterizedType.create(Consumer.class, ClassType.create(
+ DotName.createSimple(WebSocketTelemetryProviderBuilder.class))) },
+ null))
+ .createWith(recorder.createTelemetryProvider())
+ .scope(Singleton.class)
+ .done();
+ syntheticBeanProducer.produce(syntheticBeanBuildItem);
+ }
+ }
+
+ private static boolean isTracesSupportEnabled(Capabilities capabilities) {
+ return capabilities.isPresent(Capability.OPENTELEMETRY_TRACER);
+ }
+
private static Map collectEndpointSecurityChecks(List endpoints,
ClassSecurityCheckStorageBuildItem storage, IndexView index) {
return endpoints
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/InMemorySpanExporterProducer.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/InMemorySpanExporterProducer.java
new file mode 100644
index 00000000000000..dc4f4d3997c3a9
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/InMemorySpanExporterProducer.java
@@ -0,0 +1,18 @@
+package io.quarkus.websockets.next.test.telemetry;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+import jakarta.inject.Singleton;
+
+import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
+
+@ApplicationScoped
+public class InMemorySpanExporterProducer {
+
+ @Produces
+ @Singleton
+ InMemorySpanExporter inMemorySpanExporter() {
+ return InMemorySpanExporter.create();
+ }
+
+}
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
new file mode 100644
index 00000000000000..c55bde343fa924
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/OpenTelemetryWebSocketsTest.java
@@ -0,0 +1,214 @@
+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;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.inject.Inject;
+
+import org.awaitility.Awaitility;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.quarkus.builder.Version;
+import io.quarkus.maven.dependency.Dependency;
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.websockets.next.WebSocketConnector;
+import io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage.BounceClient;
+import io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage.BounceEndpoint;
+import io.quarkus.websockets.next.test.utils.WSClient;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.WebSocketConnectOptions;
+
+public class OpenTelemetryWebSocketsTest {
+
+ @RegisterExtension
+ public static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> root
+ .addClasses(BounceEndpoint.class, WSClient.class, InMemorySpanExporterProducer.class, BounceClient.class)
+ .addAsResource(new StringAsset("""
+ quarkus.otel.bsp.export.timeout=1s
+ quarkus.otel.bsp.schedule.delay=50
+ """), "application.properties"))
+ .setForcedDependencies(
+ List.of(Dependency.of("io.quarkus", "quarkus-opentelemetry-deployment", Version.getVersion())));
+
+ @TestHTTPResource("bounce")
+ URI bounceUri;
+
+ @TestHTTPResource("/")
+ URI baseUri;
+
+ @Inject
+ Vertx vertx;
+
+ @Inject
+ InMemorySpanExporter spanExporter;
+
+ @Inject
+ WebSocketConnector connector;
+
+ @BeforeEach
+ public void resetSpans() {
+ spanExporter.reset();
+ BounceEndpoint.connectionId = null;
+ BounceEndpoint.endpointId = null;
+ BounceEndpoint.MESSAGES.clear();
+ BounceClient.MESSAGES.clear();
+ BounceClient.CLOSED_LATCH = new CountDownLatch(1);
+ BounceEndpoint.CLOSED_LATCH = new CountDownLatch(1);
+ }
+
+ @Test
+ public void testServerEndpointTracesOnly() {
+ assertEquals(0, spanExporter.getFinishedSpanItems().size());
+ try (WSClient client = new WSClient(vertx)) {
+ client.connect(new WebSocketConnectOptions(), bounceUri);
+ var response = client.sendAndAwaitReply("How U Livin'").toString();
+ assertEquals("How U Livin'", response);
+ }
+ waitForTracesToArrive(3);
+ var initialRequestSpan = getSpanByName("GET /bounce", SpanKind.SERVER);
+
+ var connectionOpenedSpan = getSpanByName("OPEN " + bounceUri.getPath(), SpanKind.SERVER);
+ assertEquals(bounceUri.getPath(), getUriAttrVal(connectionOpenedSpan));
+ assertEquals(initialRequestSpan.getSpanId(), connectionOpenedSpan.getLinks().get(0).getSpanContext().getSpanId());
+
+ var connectionClosedSpan = getSpanByName("CLOSE " + bounceUri.getPath(), SpanKind.SERVER);
+ assertEquals(bounceUri.getPath(), getUriAttrVal(connectionClosedSpan));
+ assertEquals(BounceEndpoint.connectionId, getConnectionIdAttrVal(connectionClosedSpan));
+ assertEquals(BounceEndpoint.endpointId, getEndpointIdAttrVal(connectionClosedSpan));
+ assertEquals(1, connectionClosedSpan.getLinks().size());
+ assertEquals(connectionOpenedSpan.getSpanId(), connectionClosedSpan.getLinks().get(0).getSpanContext().getSpanId());
+ }
+
+ @Test
+ public void testClientAndServerEndpointTraces() throws InterruptedException {
+ var clientConn = connector.baseUri(baseUri).connectAndAwait();
+ clientConn.sendTextAndAwait("Make It Bun Dem");
+
+ // assert client and server called
+ Awaitility.await().untilAsserted(() -> {
+ assertEquals(1, BounceEndpoint.MESSAGES.size());
+ assertEquals("Make It Bun Dem", BounceEndpoint.MESSAGES.get(0));
+ assertEquals(1, BounceClient.MESSAGES.size());
+ assertEquals("Make It Bun Dem", BounceClient.MESSAGES.get(0));
+ });
+
+ clientConn.closeAndAwait();
+ // assert connection closed and client/server were notified
+ assertTrue(BounceClient.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
+ assertTrue(BounceEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
+
+ waitForTracesToArrive(5);
+
+ // server traces
+ var initialRequestSpan = getSpanByName("GET /bounce", SpanKind.SERVER);
+ var connectionOpenedSpan = getSpanByName("OPEN " + bounceUri.getPath(), SpanKind.SERVER);
+ assertEquals(bounceUri.getPath(), getUriAttrVal(connectionOpenedSpan));
+ assertEquals(initialRequestSpan.getSpanId(), connectionOpenedSpan.getLinks().get(0).getSpanContext().getSpanId());
+ var connectionClosedSpan = getSpanByName("CLOSE " + bounceUri.getPath(), SpanKind.SERVER);
+ assertEquals(bounceUri.getPath(), getUriAttrVal(connectionClosedSpan));
+ assertEquals(BounceEndpoint.connectionId, getConnectionIdAttrVal(connectionClosedSpan));
+ assertEquals(BounceEndpoint.endpointId, getEndpointIdAttrVal(connectionClosedSpan));
+ assertEquals(1, connectionClosedSpan.getLinks().size());
+ assertEquals(connectionOpenedSpan.getSpanId(), connectionClosedSpan.getLinks().get(0).getSpanContext().getSpanId());
+
+ // client traces
+ connectionOpenedSpan = getSpanByName("OPEN " + bounceUri.getPath(), SpanKind.CLIENT);
+ assertEquals(bounceUri.getPath(), getUriAttrVal(connectionOpenedSpan));
+ assertTrue(connectionOpenedSpan.getLinks().isEmpty());
+ connectionClosedSpan = getSpanByName("CLOSE " + bounceUri.getPath(), SpanKind.CLIENT);
+ assertEquals(bounceUri.getPath(), getUriAttrVal(connectionClosedSpan));
+ assertNotNull(getConnectionIdAttrVal(connectionClosedSpan));
+ assertNotNull(getClientIdAttrVal(connectionClosedSpan));
+ assertEquals(1, connectionClosedSpan.getLinks().size());
+ assertEquals(connectionOpenedSpan.getSpanId(), connectionClosedSpan.getLinks().get(0).getSpanContext().getSpanId());
+ }
+
+ @Test
+ public void testServerTracesWhenErrorOnMessage() {
+ assertEquals(0, spanExporter.getFinishedSpanItems().size());
+ try (WSClient client = new WSClient(vertx)) {
+ client.connect(new WebSocketConnectOptions(), bounceUri);
+ var response = client.sendAndAwaitReply("It's Alright, Ma").toString();
+ assertEquals("It's Alright, Ma", response);
+ response = client.sendAndAwaitReply("I'm Only Bleeding").toString();
+ assertEquals("I'm Only Bleeding", response);
+
+ client.sendAndAwait("throw-exception");
+ Awaitility.await().atMost(Duration.ofSeconds(5)).until(client::isClosed);
+ assertEquals(WebSocketCloseStatus.INTERNAL_SERVER_ERROR.code(), client.closeStatusCode());
+ }
+ waitForTracesToArrive(3);
+
+ // server traces
+ var initialRequestSpan = getSpanByName("GET /bounce", SpanKind.SERVER);
+ var connectionOpenedSpan = getSpanByName("OPEN " + bounceUri.getPath(), SpanKind.SERVER);
+ assertEquals(bounceUri.getPath(), getUriAttrVal(connectionOpenedSpan));
+ assertEquals(initialRequestSpan.getSpanId(), connectionOpenedSpan.getLinks().get(0).getSpanContext().getSpanId());
+ var connectionClosedSpan = getSpanByName("CLOSE " + bounceUri.getPath(), SpanKind.SERVER);
+ assertEquals(bounceUri.getPath(), getUriAttrVal(connectionClosedSpan));
+ assertEquals(BounceEndpoint.connectionId, getConnectionIdAttrVal(connectionClosedSpan));
+ assertEquals(BounceEndpoint.endpointId, getEndpointIdAttrVal(connectionClosedSpan));
+ assertEquals(1, connectionClosedSpan.getLinks().size());
+ assertEquals(connectionOpenedSpan.getSpanId(), connectionClosedSpan.getLinks().get(0).getSpanContext().getSpanId());
+ }
+
+ private String getConnectionIdAttrVal(SpanData connectionOpenedSpan) {
+ return connectionOpenedSpan
+ .getAttributes()
+ .get(AttributeKey.stringKey(CONNECTION_ID_ATTR_KEY));
+ }
+
+ private String getClientIdAttrVal(SpanData connectionOpenedSpan) {
+ return connectionOpenedSpan
+ .getAttributes()
+ .get(AttributeKey.stringKey(CONNECTION_CLIENT_ATTR_KEY));
+ }
+
+ private String getUriAttrVal(SpanData connectionOpenedSpan) {
+ return connectionOpenedSpan.getAttributes().get(URL_PATH);
+ }
+
+ private String getEndpointIdAttrVal(SpanData connectionOpenedSpan) {
+ return connectionOpenedSpan
+ .getAttributes()
+ .get(AttributeKey.stringKey(CONNECTION_ENDPOINT_ATTR_KEY));
+ }
+
+ private void waitForTracesToArrive(int expectedTracesCount) {
+ Awaitility.await()
+ .atMost(Duration.ofSeconds(5))
+ .untilAsserted(() -> assertEquals(expectedTracesCount, spanExporter.getFinishedSpanItems().size()));
+ }
+
+ private SpanData getSpanByName(String name, SpanKind kind) {
+ return spanExporter.getFinishedSpanItems()
+ .stream()
+ .filter(sd -> name.equals(sd.getName()))
+ .filter(sd -> sd.getKind() == kind)
+ .findFirst()
+ .orElseThrow(() -> new AssertionError(
+ "Expected span name '" + name + "' and kind '" + kind + "' not found: "
+ + spanExporter.getFinishedSpanItems()));
+ }
+}
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/TracesDisabledWebSocketsTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/TracesDisabledWebSocketsTest.java
new file mode 100644
index 00000000000000..f5fce20db188c0
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/TracesDisabledWebSocketsTest.java
@@ -0,0 +1,138 @@
+package io.quarkus.websockets.next.test.telemetry;
+
+import static io.opentelemetry.semconv.UrlAttributes.URL_PATH;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.inject.Inject;
+
+import org.awaitility.Awaitility;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.quarkus.builder.Version;
+import io.quarkus.maven.dependency.Dependency;
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.websockets.next.WebSocketConnector;
+import io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage.BounceClient;
+import io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage.BounceEndpoint;
+import io.quarkus.websockets.next.test.utils.WSClient;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.WebSocketConnectOptions;
+
+public class TracesDisabledWebSocketsTest {
+
+ @RegisterExtension
+ public static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> root
+ .addClasses(BounceEndpoint.class, WSClient.class, InMemorySpanExporterProducer.class, BounceClient.class)
+ .addAsResource(new StringAsset("""
+ quarkus.otel.bsp.export.timeout=1s
+ quarkus.otel.bsp.schedule.delay=50
+ quarkus.websockets-next.server.traces.enabled=false
+ quarkus.websockets-next.client.traces.enabled=false
+ """), "application.properties"))
+ .setForcedDependencies(
+ List.of(Dependency.of("io.quarkus", "quarkus-opentelemetry-deployment", Version.getVersion())));
+
+ @TestHTTPResource("bounce")
+ URI bounceUri;
+
+ @TestHTTPResource
+ URI baseUri;
+
+ @Inject
+ Vertx vertx;
+
+ @Inject
+ InMemorySpanExporter spanExporter;
+
+ @Inject
+ WebSocketConnector connector;
+
+ @BeforeEach
+ public void resetSpans() {
+ spanExporter.reset();
+ BounceEndpoint.connectionId = null;
+ BounceEndpoint.endpointId = null;
+ BounceEndpoint.MESSAGES.clear();
+ BounceClient.MESSAGES.clear();
+ BounceClient.CLOSED_LATCH = new CountDownLatch(1);
+ BounceEndpoint.CLOSED_LATCH = new CountDownLatch(1);
+ }
+
+ @Test
+ public void testServerEndpointTracesDisabled() {
+ assertEquals(0, spanExporter.getFinishedSpanItems().size());
+ try (WSClient client = new WSClient(vertx)) {
+ client.connect(new WebSocketConnectOptions(), bounceUri);
+ var response = client.sendAndAwaitReply("How U Livin'").toString();
+ assertEquals("How U Livin'", response);
+ }
+ waitForInitialRequestTrace();
+
+ // check HTTP server traces still enabled
+ var initialRequestSpan = getInitialRequestSpan();
+ assertEquals(bounceUri.getPath(), initialRequestSpan.getAttributes().get(URL_PATH));
+
+ // check WebSocket server endpoint traces are disabled
+ assertEquals(1, spanExporter.getFinishedSpanItems().size());
+ }
+
+ @Test
+ public void testClientAndServerEndpointTracesDisabled() throws InterruptedException {
+ var clientConn = connector.baseUri(baseUri).connectAndAwait();
+ clientConn.sendTextAndAwait("Make It Bun Dem");
+
+ // assert client and server called
+ Awaitility.await().untilAsserted(() -> {
+ assertEquals(1, BounceEndpoint.MESSAGES.size());
+ assertEquals("Make It Bun Dem", BounceEndpoint.MESSAGES.get(0));
+ assertEquals(1, BounceClient.MESSAGES.size());
+ assertEquals("Make It Bun Dem", BounceClient.MESSAGES.get(0));
+ });
+
+ clientConn.closeAndAwait();
+ // assert connection closed and client/server were notified
+ assertTrue(BounceClient.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
+ assertTrue(BounceEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
+
+ waitForInitialRequestTrace();
+
+ // check HTTP server traces still enabled
+ var initialRequestSpan = getInitialRequestSpan();
+ assertEquals("", initialRequestSpan.getAttributes().get(URL_PATH));
+
+ // check both client and server WebSocket endpoint traces are disabled
+ assertEquals(1, spanExporter.getFinishedSpanItems().size());
+ }
+
+ private void waitForInitialRequestTrace() {
+ Awaitility.await()
+ .atMost(Duration.ofSeconds(5))
+ .untilAsserted(() -> assertEquals(1, spanExporter.getFinishedSpanItems().size()));
+ }
+
+ private SpanData getInitialRequestSpan() {
+ return spanExporter.getFinishedSpanItems()
+ .stream()
+ .filter(sd -> "GET /bounce".equals(sd.getName()))
+ .filter(sd -> sd.getKind() == SpanKind.SERVER)
+ .findFirst()
+ .orElseThrow(() -> new AssertionError(
+ "Expected span name 'GET /bounce' and kind '" + SpanKind.SERVER + "' not found: "
+ + spanExporter.getFinishedSpanItems()));
+ }
+}
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/BounceClient.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/BounceClient.java
new file mode 100644
index 00000000000000..aba3c46ab0d794
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/BounceClient.java
@@ -0,0 +1,27 @@
+package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+
+import io.quarkus.websockets.next.OnClose;
+import io.quarkus.websockets.next.OnTextMessage;
+import io.quarkus.websockets.next.WebSocketClient;
+
+@WebSocketClient(path = "/bounce", clientId = "bounce-client-id")
+public class BounceClient {
+
+ public static List MESSAGES = new CopyOnWriteArrayList<>();
+ public static CountDownLatch CLOSED_LATCH = new CountDownLatch(1);
+
+ @OnTextMessage
+ void echo(String message) {
+ MESSAGES.add(message);
+ }
+
+ @OnClose
+ void onClose() {
+ CLOSED_LATCH.countDown();
+ }
+
+}
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/BounceEndpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/BounceEndpoint.java
new file mode 100644
index 00000000000000..6f5527583dc1a1
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/BounceEndpoint.java
@@ -0,0 +1,49 @@
+package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import io.quarkus.websockets.next.OnClose;
+import io.quarkus.websockets.next.OnOpen;
+import io.quarkus.websockets.next.OnTextMessage;
+import io.quarkus.websockets.next.WebSocket;
+import io.quarkus.websockets.next.WebSocketConnection;
+
+@WebSocket(path = "/bounce", endpointId = "bounce-server-endpoint-id")
+public class BounceEndpoint {
+
+ public static final List MESSAGES = new CopyOnWriteArrayList<>();
+ public static CountDownLatch CLOSED_LATCH = new CountDownLatch(1);
+ public static volatile String connectionId = null;
+ public static volatile String endpointId = null;
+
+ @ConfigProperty(name = "bounce-endpoint.prefix-responses", defaultValue = "false")
+ boolean prefixResponses;
+
+ @OnTextMessage
+ public String onMessage(String message) {
+ if (prefixResponses) {
+ message = "echo 0: " + message;
+ }
+ MESSAGES.add(message);
+ if (message.equals("throw-exception")) {
+ throw new RuntimeException("Failing 'onMessage' to test behavior when an exception was thrown");
+ }
+ return message;
+ }
+
+ @OnOpen
+ void open(WebSocketConnection connection) {
+ connectionId = connection.id();
+ endpointId = connection.endpointId();
+ }
+
+ @OnClose
+ void onClose() {
+ CLOSED_LATCH.countDown();
+ }
+
+}
diff --git a/extensions/websockets-next/runtime/pom.xml b/extensions/websockets-next/runtime/pom.xml
index 4f0487b5905997..b3199acdd0bd38 100644
--- a/extensions/websockets-next/runtime/pom.xml
+++ b/extensions/websockets-next/runtime/pom.xml
@@ -43,6 +43,17 @@
io.quarkus.security
quarkus-security
+
+
+ io.opentelemetry
+ opentelemetry-api
+ true
+
+
+ io.opentelemetry.semconv
+ opentelemetry-semconv
+ true
+
org.junit.jupiter
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/TelemetryConfig.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/TelemetryConfig.java
new file mode 100644
index 00000000000000..4bbdd21d268469
--- /dev/null
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/TelemetryConfig.java
@@ -0,0 +1,19 @@
+package io.quarkus.websockets.next;
+
+import io.smallrye.config.WithDefault;
+import io.smallrye.config.WithName;
+
+/**
+ * Configures telemetry in the WebSockets extension.
+ */
+public interface TelemetryConfig {
+
+ /**
+ * If collection of WebSocket traces is enabled.
+ * Only applicable when the OpenTelemetry extension is present.
+ */
+ @WithName("traces.enabled")
+ @WithDefault("true")
+ boolean tracesEnabled();
+
+}
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsClientRuntimeConfig.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsClientRuntimeConfig.java
index 30ad1b84f474a4..4db5076d36cb6e 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsClientRuntimeConfig.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsClientRuntimeConfig.java
@@ -8,6 +8,7 @@
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
+import io.smallrye.config.WithParentName;
@ConfigMapping(prefix = "quarkus.websockets-next.client")
@ConfigRoot(phase = ConfigPhase.RUN_TIME)
@@ -66,4 +67,10 @@ public interface WebSocketsClientRuntimeConfig {
*/
TrafficLoggingConfig trafficLogging();
+ /**
+ * Telemetry configuration.
+ */
+ @WithParentName
+ TelemetryConfig telemetry();
+
}
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java
index a5df5c23dd1046..cfd403b4724681 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java
@@ -9,6 +9,7 @@
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
+import io.smallrye.config.WithParentName;
@ConfigMapping(prefix = "quarkus.websockets-next.server")
@ConfigRoot(phase = ConfigPhase.RUN_TIME)
@@ -69,6 +70,12 @@ public interface WebSocketsServerRuntimeConfig {
*/
TrafficLoggingConfig trafficLogging();
+ /**
+ * Telemetry configuration.
+ */
+ @WithParentName
+ TelemetryConfig telemetry();
+
interface Security {
/**
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java
index 7ccc97539e7f28..6ff0bf6b1116f5 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java
@@ -1,5 +1,6 @@
package io.quarkus.websockets.next.runtime;
+import java.lang.reflect.InvocationTargetException;
import java.time.Duration;
import java.util.Optional;
import java.util.function.Consumer;
@@ -19,6 +20,7 @@
import io.quarkus.websockets.next.UnhandledFailureStrategy;
import io.quarkus.websockets.next.WebSocketException;
import io.quarkus.websockets.next.runtime.WebSocketSessionContext.SessionContextState;
+import io.quarkus.websockets.next.runtime.telemetry.TelemetrySupport;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.operators.multi.processors.BroadcastProcessor;
import io.vertx.core.Context;
@@ -34,7 +36,7 @@ class Endpoints {
static void initialize(Vertx vertx, ArcContainer container, Codecs codecs, WebSocketConnectionBase connection,
WebSocketBase ws, String generatedEndpointClass, Optional autoPingInterval,
SecuritySupport securitySupport, UnhandledFailureStrategy unhandledFailureStrategy, TrafficLogger trafficLogger,
- Runnable onClose, boolean activateRequestContext) {
+ Runnable onClose, boolean activateRequestContext, TelemetrySupport telemetrySupport) {
Context context = vertx.getOrCreateContext();
@@ -48,7 +50,7 @@ static void initialize(Vertx vertx, ArcContainer container, Codecs codecs, WebSo
// Create an endpoint that delegates callbacks to the endpoint bean
WebSocketEndpoint endpoint = createEndpoint(generatedEndpointClass, context, connection, codecs, contextSupport,
- securitySupport);
+ securitySupport, telemetrySupport);
// A broadcast processor is only needed if Multi is consumed by the callback
BroadcastProcessor