diff --git a/docs/src/main/asciidoc/opentelemetry-tracing.adoc b/docs/src/main/asciidoc/opentelemetry-tracing.adoc index 088dbbf14a020..84422e79f27ed 100644 --- a/docs/src/main/asciidoc/opentelemetry-tracing.adoc +++ b/docs/src/main/asciidoc/opentelemetry-tracing.adoc @@ -600,6 +600,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/telemetry-micrometer.adoc b/docs/src/main/asciidoc/telemetry-micrometer.adoc index a61966fa16d76..7b990488eae57 100644 --- a/docs/src/main/asciidoc/telemetry-micrometer.adoc +++ b/docs/src/main/asciidoc/telemetry-micrometer.adoc @@ -761,6 +761,7 @@ Refer to the xref:./management-interface-reference.adoc[management interface ref ** Camel Messaging * https://quarkus.io/guides/stork-reference[`quarkus-smallrye-stork`] * https://quarkus.io/guides/vertx[`quarkus-vertx`] (http requests) +* xref:websockets-next-reference.adoc[`websockets-next`] == Configuration Reference diff --git a/docs/src/main/asciidoc/websockets-next-reference.adoc b/docs/src/main/asciidoc/websockets-next-reference.adoc index 779057a83e110..be6bd2e2c9814 100644 --- a/docs/src/main/asciidoc/websockets-next-reference.adoc +++ b/docs/src/main/asciidoc/websockets-next-reference.adoc @@ -1018,6 +1018,28 @@ 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 +---- + +When the Micrometer extension is present, metrics for messages, errors and bytes transferred are collected. +If you do not require WebSocket metrics, you can disable metrics like in the example below: + +[source, properties] +---- +quarkus.websockets-next.server.metrics.enabled=false +quarkus.websockets-next.client.metrics.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 7681fcf852e7b..4600d11832fa5 100644 --- a/extensions/websockets-next/deployment/pom.xml +++ b/extensions/websockets-next/deployment/pom.xml @@ -80,6 +80,12 @@ mutiny-kotlin test + + + io.opentelemetry + opentelemetry-sdk-testing + test + diff --git a/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/TelemetrySupportBuilderCustomizerBuildItem.java b/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/TelemetrySupportBuilderCustomizerBuildItem.java new file mode 100644 index 0000000000000..e80d74815312d --- /dev/null +++ b/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/TelemetrySupportBuilderCustomizerBuildItem.java @@ -0,0 +1,18 @@ +package io.quarkus.websockets.next.deployment; + +import java.util.function.Consumer; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.websockets.next.runtime.telemetry.TelemetrySupportProviderBuilder; + +/** + * Provides a way to set up metrics and/or traces support in the WebSockets extension. + */ +final class TelemetrySupportBuilderCustomizerBuildItem extends MultiBuildItem { + + final Consumer builderCustomizer; + + TelemetrySupportBuilderCustomizerBuildItem(Consumer builderCustomizer) { + this.builderCustomizer = builderCustomizer; + } +} 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 cf2040a7e2c2a..af6418b0b354b 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 @@ -18,6 +18,7 @@ import jakarta.enterprise.context.SessionScoped; import jakarta.enterprise.invoke.Invoker; +import jakarta.inject.Singleton; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTransformation; @@ -72,6 +73,7 @@ import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.execannotations.ExecutionModelAnnotationsAllowedBuildItem; +import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.CatchBlockCreator; import io.quarkus.gizmo.ClassCreator; @@ -82,6 +84,7 @@ import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.gizmo.TryBlock; +import io.quarkus.runtime.metrics.MetricsFactory; import io.quarkus.security.spi.ClassSecurityCheckAnnotationBuildItem; import io.quarkus.security.spi.ClassSecurityCheckStorageBuildItem; import io.quarkus.security.spi.SecurityTransformerUtils; @@ -118,6 +121,11 @@ 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.ErrorInterceptor; +import io.quarkus.websockets.next.runtime.telemetry.MetricsBuilderCustomizer; +import io.quarkus.websockets.next.runtime.telemetry.TelemetrySupportProvider; +import io.quarkus.websockets.next.runtime.telemetry.TracesBuilderCustomizer; +import io.quarkus.websockets.next.runtime.telemetry.WebSocketTelemetryRecorder; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.groups.UniCreate; @@ -407,7 +415,9 @@ public void generateEndpoints(BeanArchiveIndexBuildItem index, List generatedClasses, BuildProducer generatedEndpoints, - BuildProducer reflectiveClasses) { + BuildProducer reflectiveClasses, + List telemetryBuilderCustomizers) { + final boolean telemetryRequired = !telemetryBuilderCustomizers.isEmpty(); ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClasses, new Function() { @Override public String apply(String name) { @@ -432,7 +442,7 @@ public String apply(String name) { String generatedName = generateEndpoint(endpoint, argumentProviders, transformedAnnotations, index.getIndex(), classOutput, globalErrorHandlers, endpoint.isClient() ? CLIENT_ENDPOINT_SUFFIX : SERVER_ENDPOINT_SUFFIX, - invokerFactory); + invokerFactory, telemetryRequired); reflectiveClasses.produce(ReflectiveClassBuildItem.builder(generatedName).constructors().build()); generatedEndpoints .produce(new GeneratedEndpointBuildItem(endpoint.id, endpoint.bean.getImplClazz().name().toString(), @@ -459,7 +469,7 @@ public void registerRoutes(WebSocketServerRecorder recorder, List metricsCapability, + BuildProducer builderProducer) { + boolean metricsEnabled = metricsCapability.map(m -> m.metricsSupported(MetricsFactory.MICROMETER)).orElse(false); + if (metricsEnabled) { + builderProducer.produce(new TelemetrySupportBuilderCustomizerBuildItem(new MetricsBuilderCustomizer())); + } + } + + @BuildStep + void addTracesSupport(Capabilities capabilities, + BuildProducer builderProducer) { + if (capabilities.isPresent(Capability.OPENTELEMETRY_TRACER)) { + builderProducer.produce(new TelemetrySupportBuilderCustomizerBuildItem(new TracesBuilderCustomizer())); + } + } + + @BuildStep + @Record(RUNTIME_INIT) + SyntheticBeanBuildItem createTelemetrySupportProvider( + List builderCustomizerItems, WebSocketTelemetryRecorder recorder) { + var builderCustomizers = builderCustomizerItems.stream().map(i -> i.builderCustomizer).toList(); + var configurator = SyntheticBeanBuildItem + .configure(TelemetrySupportProvider.class) + .setRuntimeInit() // consumes runtime config: traces / metrics enabled + .unremovable() + .scope(Singleton.class); + if (builderCustomizers.isEmpty()) { + configurator.runtimeValue(recorder.createEmptyTelemetrySupportProvider()); + } else { + configurator.supplier(recorder.createTelemetrySupportProvider(builderCustomizers)); + } + return configurator.done(); + } + private static Map collectEndpointSecurityChecks(List endpoints, ClassSecurityCheckStorageBuildItem storage, IndexView index) { return endpoints @@ -747,7 +792,7 @@ static String generateEndpoint(WebSocketEndpointBuildItem endpoint, ClassOutput classOutput, GlobalErrorHandlersBuildItem globalErrorHandlers, String endpointSuffix, - InvokerFactoryBuildItem invokerFactory) { + InvokerFactoryBuildItem invokerFactory, boolean telemetryRequired) { ClassInfo implClazz = endpoint.bean.getImplClazz(); String baseName; if (implClazz.enclosingClass() != null) { @@ -764,12 +809,12 @@ static String generateEndpoint(WebSocketEndpointBuildItem endpoint, .build(); MethodCreator constructor = endpointCreator.getConstructorCreator(WebSocketConnectionBase.class, - Codecs.class, ContextSupport.class, SecuritySupport.class); + Codecs.class, ContextSupport.class, SecuritySupport.class, ErrorInterceptor.class); constructor.invokeSpecialMethod( MethodDescriptor.ofConstructor(WebSocketEndpointBase.class, WebSocketConnectionBase.class, - Codecs.class, ContextSupport.class, SecuritySupport.class), + Codecs.class, ContextSupport.class, SecuritySupport.class, ErrorInterceptor.class), constructor.getThis(), constructor.getMethodParam(0), constructor.getMethodParam(1), - constructor.getMethodParam(2), constructor.getMethodParam(3)); + constructor.getMethodParam(2), constructor.getMethodParam(3), constructor.getMethodParam(4)); MethodCreator inboundProcessingMode = endpointCreator.getMethodCreator("inboundProcessingMode", InboundProcessingMode.class); @@ -789,7 +834,8 @@ static String generateEndpoint(WebSocketEndpointBuildItem endpoint, ResultHandle[] args = callback.generateArguments(tryBlock.getThis(), tryBlock, transformedAnnotations, index); ResultHandle ret = callBusinessMethod(endpointCreator, constructor, callback, "Open", tryBlock, beanInstance, args, invokerFactory); - encodeAndReturnResult(tryBlock.getThis(), tryBlock, callback, globalErrorHandlers, endpoint, ret); + encodeAndReturnResult(tryBlock.getThis(), tryBlock, callback, globalErrorHandlers, endpoint, ret, + telemetryRequired); MethodCreator onOpenExecutionModel = endpointCreator.getMethodCreator("onOpenExecutionModel", ExecutionModel.class); @@ -797,11 +843,11 @@ static String generateEndpoint(WebSocketEndpointBuildItem endpoint, } generateOnMessage(endpointCreator, constructor, endpoint, endpoint.onBinaryMessage, argumentProviders, - transformedAnnotations, index, globalErrorHandlers, invokerFactory); + transformedAnnotations, index, globalErrorHandlers, invokerFactory, telemetryRequired); generateOnMessage(endpointCreator, constructor, endpoint, endpoint.onTextMessage, argumentProviders, - transformedAnnotations, index, globalErrorHandlers, invokerFactory); + transformedAnnotations, index, globalErrorHandlers, invokerFactory, telemetryRequired); generateOnMessage(endpointCreator, constructor, endpoint, endpoint.onPongMessage, argumentProviders, - transformedAnnotations, index, globalErrorHandlers, invokerFactory); + transformedAnnotations, index, globalErrorHandlers, invokerFactory, telemetryRequired); if (endpoint.onClose != null) { Callback callback = endpoint.onClose; @@ -814,7 +860,8 @@ static String generateEndpoint(WebSocketEndpointBuildItem endpoint, ResultHandle[] args = callback.generateArguments(tryBlock.getThis(), tryBlock, transformedAnnotations, index); ResultHandle ret = callBusinessMethod(endpointCreator, constructor, callback, "Close", tryBlock, beanInstance, args, invokerFactory); - encodeAndReturnResult(tryBlock.getThis(), tryBlock, callback, globalErrorHandlers, endpoint, ret); + encodeAndReturnResult(tryBlock.getThis(), tryBlock, callback, globalErrorHandlers, endpoint, ret, + telemetryRequired); MethodCreator onCloseExecutionModel = endpointCreator.getMethodCreator("onCloseExecutionModel", ExecutionModel.class); @@ -822,7 +869,7 @@ static String generateEndpoint(WebSocketEndpointBuildItem endpoint, } generateOnError(endpointCreator, constructor, endpoint, transformedAnnotations, globalErrorHandlers, index, - invokerFactory); + invokerFactory, telemetryRequired); // we write into the constructor when generating callback invokers, so need to finish it late constructor.returnVoid(); @@ -833,7 +880,8 @@ static String generateEndpoint(WebSocketEndpointBuildItem endpoint, private static void generateOnError(ClassCreator endpointCreator, MethodCreator constructor, WebSocketEndpointBuildItem endpoint, TransformedAnnotationsBuildItem transformedAnnotations, - GlobalErrorHandlersBuildItem globalErrorHandlers, IndexView index, InvokerFactoryBuildItem invokerFactory) { + GlobalErrorHandlersBuildItem globalErrorHandlers, IndexView index, InvokerFactoryBuildItem invokerFactory, + boolean telemetryRequired) { Map errors = new HashMap<>(); List throwableInfos = new ArrayList<>(); @@ -868,6 +916,13 @@ private static void generateOnError(ClassCreator endpointCreator, MethodCreator throwableInfos.sort(Comparator.comparingInt(ThrowableInfo::level).reversed()); ResultHandle endpointThis = doOnError.getThis(); + if (telemetryRequired) { + // this.interceptError(throwable); + doOnError.invokeVirtualMethod( + MethodDescriptor.ofMethod(WebSocketEndpointBase.class, "interceptError", void.class, Throwable.class), + endpointThis, doOnError.getMethodParam(0)); + } + for (ThrowableInfo throwableInfo : throwableInfos) { BytecodeCreator throwableMatches = doOnError .ifTrue(doOnError.instanceOf(doOnError.getMethodParam(0), throwableInfo.hierarchy.get(0).toString())) @@ -885,7 +940,7 @@ private static void generateOnError(ClassCreator endpointCreator, MethodCreator ResultHandle[] args = callback.generateArguments(endpointThis, tryBlock, transformedAnnotations, index); ResultHandle ret = callBusinessMethod(endpointCreator, constructor, callback, "Error", tryBlock, beanInstance, args, invokerFactory); - encodeAndReturnResult(endpointThis, tryBlock, callback, globalErrorHandlers, endpoint, ret); + encodeAndReturnResult(endpointThis, tryBlock, callback, globalErrorHandlers, endpoint, ret, telemetryRequired); // return doErrorExecute() throwableMatches.returnValue( @@ -937,7 +992,8 @@ record GlobalErrorHandler(BeanInfo bean, Callback callback) { private static void generateOnMessage(ClassCreator endpointCreator, MethodCreator constructor, WebSocketEndpointBuildItem endpoint, Callback callback, CallbackArgumentsBuildItem callbackArguments, TransformedAnnotationsBuildItem transformedAnnotations, - IndexView index, GlobalErrorHandlersBuildItem globalErrorHandlers, InvokerFactoryBuildItem invokerFactory) { + IndexView index, GlobalErrorHandlersBuildItem globalErrorHandlers, InvokerFactoryBuildItem invokerFactory, + boolean telemetryRequired) { if (callback == null) { return; } @@ -970,7 +1026,7 @@ private static void generateOnMessage(ClassCreator endpointCreator, MethodCreato // Call the business method ResultHandle ret = callBusinessMethod(endpointCreator, constructor, callback, messageType, tryBlock, beanInstance, args, invokerFactory); - encodeAndReturnResult(tryBlock.getThis(), tryBlock, callback, globalErrorHandlers, endpoint, ret); + encodeAndReturnResult(tryBlock.getThis(), tryBlock, callback, globalErrorHandlers, endpoint, ret, telemetryRequired); MethodCreator onMessageExecutionModel = endpointCreator.getMethodCreator("on" + messageType + "MessageExecutionModel", ExecutionModel.class); @@ -1113,11 +1169,17 @@ static ResultHandle decodeMessage( } private static ResultHandle uniOnFailureDoOnError(ResultHandle endpointThis, BytecodeCreator method, Callback callback, - ResultHandle uni, WebSocketEndpointBuildItem endpoint, GlobalErrorHandlersBuildItem globalErrorHandlers) { + ResultHandle uni, WebSocketEndpointBuildItem endpoint, GlobalErrorHandlersBuildItem globalErrorHandlers, + boolean telemetryRequired) { + if (callback.isOnError() || (globalErrorHandlers.handlers.isEmpty() && (endpoint == null || endpoint.onErrors.isEmpty()))) { // @OnError or no error handlers available - return uni; + // but when telemetry is required, we need 'doOnError' to be always called so that we have one method + // that is always called (and intercepted) on error + if (!telemetryRequired) { + return uni; + } } // return uniMessage.onFailure().recoverWithUni(t -> { // return doOnError(t); @@ -1136,7 +1198,7 @@ private static ResultHandle uniOnFailureDoOnError(ResultHandle endpointThis, Byt private static ResultHandle encodeMessage(ResultHandle endpointThis, BytecodeCreator method, Callback callback, GlobalErrorHandlersBuildItem globalErrorHandlers, WebSocketEndpointBuildItem endpoint, - ResultHandle value) { + ResultHandle value, boolean telemetryRequired) { if (callback.acceptsBinaryMessage() || isOnOpenWithBinaryReturnType(callback)) { // ---------------------- @@ -1153,7 +1215,8 @@ private static ResultHandle encodeMessage(ResultHandle endpointThis, BytecodeCre } if (messageType.name().equals(WebSocketDotNames.VOID)) { // Uni - return uniOnFailureDoOnError(endpointThis, method, callback, value, endpoint, globalErrorHandlers); + return uniOnFailureDoOnError(endpointThis, method, callback, value, endpoint, globalErrorHandlers, + telemetryRequired); } else { // return uniMessage.chain(m -> { // Buffer buffer = encodeBuffer(m); @@ -1171,7 +1234,8 @@ private static ResultHandle encodeMessage(ResultHandle endpointThis, BytecodeCre ResultHandle uniChain = method.invokeInterfaceMethod( MethodDescriptor.ofMethod(Uni.class, "chain", Uni.class, Function.class), value, fun.getInstance()); - return uniOnFailureDoOnError(endpointThis, method, callback, uniChain, endpoint, globalErrorHandlers); + return uniOnFailureDoOnError(endpointThis, method, callback, uniChain, endpoint, globalErrorHandlers, + telemetryRequired); } } else if (callback.isReturnTypeMulti()) { // try { @@ -1219,7 +1283,8 @@ private static ResultHandle encodeMessage(ResultHandle endpointThis, BytecodeCre } if (messageType.name().equals(WebSocketDotNames.VOID)) { // Uni - return uniOnFailureDoOnError(endpointThis, method, callback, value, endpoint, globalErrorHandlers); + return uniOnFailureDoOnError(endpointThis, method, callback, value, endpoint, globalErrorHandlers, + telemetryRequired); } else { // return uniMessage.chain(m -> { // String text = encodeText(m); @@ -1237,7 +1302,8 @@ private static ResultHandle encodeMessage(ResultHandle endpointThis, BytecodeCre ResultHandle uniChain = method.invokeInterfaceMethod( MethodDescriptor.ofMethod(Uni.class, "chain", Uni.class, Function.class), value, fun.getInstance()); - return uniOnFailureDoOnError(endpointThis, method, callback, uniChain, endpoint, globalErrorHandlers); + return uniOnFailureDoOnError(endpointThis, method, callback, uniChain, endpoint, globalErrorHandlers, + telemetryRequired); } } else if (callback.isReturnTypeMulti()) { // return multiText(multi, m -> { @@ -1339,7 +1405,7 @@ private static ResultHandle uniVoid(BytecodeCreator method) { private static void encodeAndReturnResult(ResultHandle endpointThis, BytecodeCreator method, Callback callback, GlobalErrorHandlersBuildItem globalErrorHandlers, WebSocketEndpointBuildItem endpoint, - ResultHandle result) { + ResultHandle result, boolean telemetryRequired) { // The result must be always Uni if (callback.isReturnTypeVoid()) { // return Uni.createFrom().void() @@ -1348,7 +1414,8 @@ private static void encodeAndReturnResult(ResultHandle endpointThis, BytecodeCre // Skip response BytecodeCreator isNull = method.ifNull(result).trueBranch(); isNull.returnValue(uniVoid(isNull)); - method.returnValue(encodeMessage(endpointThis, method, callback, globalErrorHandlers, endpoint, result)); + method.returnValue( + encodeMessage(endpointThis, method, callback, globalErrorHandlers, endpoint, result, telemetryRequired)); } } diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/AbstractWebSocketsOnMessageTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/AbstractWebSocketsOnMessageTest.java new file mode 100644 index 0000000000000..e2742193f9c8e --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/AbstractWebSocketsOnMessageTest.java @@ -0,0 +1,384 @@ +package io.quarkus.websockets.next.test.telemetry; + +import static io.quarkus.websockets.next.test.telemetry.Connection.sendAndAssertResponses; +import static io.quarkus.websockets.next.test.telemetry.ExpectedServerEndpointResponse.DOUBLE_ECHO_RESPONSE; +import static io.quarkus.websockets.next.test.telemetry.ExpectedServerEndpointResponse.ECHO_RESPONSE; +import static io.quarkus.websockets.next.test.telemetry.ExpectedServerEndpointResponse.NO_RESPONSE; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertClientConnectionClosedTotal; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertClientConnectionOpenedTotal; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertClientErrorTotal; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertClientMessagesCountBytesReceived; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertClientMessagesCountBytesSent; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertClientMessagesCountSent; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertMetrics; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertServerConnectionClosedTotal; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertServerConnectionOpenedTotal; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertServerErrorTotal; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertServerMessagesCountBytesReceived; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertServerMessagesCountBytesSent; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertServerMessagesCountReceived; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.stringToBytes; +import static io.quarkus.websockets.next.test.utils.WSClient.ReceiverMode.BINARY; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +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.WebSocketClientConnection; +import io.quarkus.websockets.next.WebSocketConnector; +import io.quarkus.websockets.next.test.telemetry.ExpectedServerEndpointResponse.DoubleEchoExpectedServerEndpointResponse; +import io.quarkus.websockets.next.test.telemetry.ExpectedServerEndpointResponse.EchoExpectedServerEndpointResponse; +import io.quarkus.websockets.next.test.telemetry.ExpectedServerEndpointResponse.NoExpectedServerEndpointResponse; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.WebSocketConnectOptions; + +public abstract class AbstractWebSocketsOnMessageTest { + + static QuarkusUnitTest createQuarkusUnitTest(String endpointsPackage) { + return new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addPackage(endpointsPackage) + .addClasses(WSClient.class, Connection.class, MetricsAsserter.class, + AbstractWebSocketsOnMessageTest.class, ExpectedServerEndpointResponse.class, + NoExpectedServerEndpointResponse.class, EchoExpectedServerEndpointResponse.class, + DoubleEchoExpectedServerEndpointResponse.class) + .addAsResource(new StringAsset(""" + bounce-endpoint.prefix-responses=true + """), "application.properties")) + .setForcedDependencies( + List.of(Dependency.of("io.quarkus", "quarkus-micrometer-registry-prometheus-deployment", + Version.getVersion()))); + } + + protected final MetricsAsserter asserter = new MetricsAsserter(); + + @TestHTTPResource("bounce") + URI bounceUri; + + @TestHTTPResource("/") + URI baseUri; + + @TestHTTPResource("received-multi-text-response-none-2") + URI multiTextReceived_NoResponse_Uri_2; + + @TestHTTPResource("broadcast") + URI broadcast_Uri; + + @Inject + Vertx vertx; + + protected abstract boolean binaryMode(); + + protected abstract WebSocketConnector bounceClientConnector(); + + protected abstract WebSocketConnector multiClientConnector(); + + @ParameterizedTest + @MethodSource("provideServerEndpointDescription") + public void testServerEndpoint(String path, String[] messagesToSend, String[] expectedResponses) { + final WSClient client = binaryMode() ? new WSClient(vertx, BINARY) : new WSClient(vertx); + try (client) { + client.connect(new WebSocketConnectOptions(), baseUri.resolve(path)); + for (String message : messagesToSend) { + if (binaryMode()) { + client.sendAndAwait(Buffer.buffer(message)); + } else { + client.sendAndAwait(message); + } + } + client.waitForMessages(expectedResponses.length); + var actualResponses = client.getMessages().stream().map(Buffer::toString).collect(Collectors.toSet()); + for (String expectedResponse : expectedResponses) { + assertTrue(actualResponses.contains(expectedResponse), + () -> "Expected response '%s' not found, was: %s".formatted(expectedResponse, actualResponses)); + } + } + + int serverReceivedCountDelta = messagesToSend.length; + int serverReceivedCountBytesDelta = stringToBytes(messagesToSend); + int serverSentCountBytesDelta = stringToBytes(expectedResponses); + + asserter.serverReceivedCount += serverReceivedCountDelta; + asserter.serverReceivedCountBytes += serverReceivedCountBytesDelta; + asserter.serverSentCountBytes += serverSentCountBytesDelta; + asserter.serverConnectionOpenedCount += 1; + + assertMetrics(metrics -> metrics + // test metrics per all the paths (regardless of the URI tag) + .body(assertServerConnectionOpenedTotal(asserter.serverConnectionOpenedCount)) + .body(assertClientConnectionOpenedTotal(asserter.clientConnectionOpenedCount)) + .body(assertServerErrorTotal(asserter.serverErrorCount)) + .body(assertClientErrorTotal(asserter.clientErrorCount)) + .body(assertClientMessagesCountBytesSent(asserter.clientSentCountBytes)) + .body(assertClientMessagesCountBytesReceived(asserter.clientReceivedCountBytes)) + .body(assertClientMessagesCountSent(asserter.clientSentCount)) + .body(assertServerMessagesCountBytesReceived(asserter.serverReceivedCountBytes)) + .body(assertServerMessagesCountBytesSent(asserter.serverSentCountBytes)) + .body(assertServerMessagesCountReceived(asserter.serverReceivedCount)) + // test metrics per path tag + .body(assertServerConnectionOpenedTotal(path, 1)) + .body(assertServerConnectionClosedTotal(path, 1)) + .body(assertClientConnectionOpenedTotal(path, 0)) + .body(assertClientConnectionClosedTotal(path, 0)) + .body(assertServerErrorTotal(path, 0)) + .body(assertClientErrorTotal(path, 0)) + .body(assertClientMessagesCountBytesSent(path, 0)) + .body(assertClientMessagesCountBytesReceived(path, 0)) + .body(assertClientMessagesCountSent(path, 0)) + .body(assertServerMessagesCountBytesReceived(path, serverReceivedCountBytesDelta)) + .body(assertServerMessagesCountBytesSent(path, serverSentCountBytesDelta)) + .body(assertServerMessagesCountReceived(path, serverReceivedCountDelta))); + } + + private static Stream provideServerEndpointDescription() { + var streamBuilder = Stream. builder(); + + // test #1: testServerEndpoint_SingleTextReceived_NoSent + // endpoint: void onMessage(String message) + String[] sentMessages = new String[] { "Ballad of a Prodigal Son" }; + streamBuilder.add(Arguments.arguments("received-single-text-response-none", sentMessages, NO_RESPONSE)); + + // test #2: testServerEndpoint_SingleTextReceived_SingleTextSent + // endpoint: String onMessage(String message) + sentMessages = new String[] { "Can't Find My Way Home" }; + streamBuilder.add(Arguments.arguments("single-text-received-single-text-sent", sentMessages, + ECHO_RESPONSE.getExpectedResponse(sentMessages))); + + // test #3: testServerEndpoint_SingleTextReceived_MultiTextSent + // endpoint: Multi onMessage(String message) + sentMessages = new String[] { "Always take a banana to a party" }; + streamBuilder.add(Arguments.arguments("received-single-text-response-multi-text", sentMessages, + DOUBLE_ECHO_RESPONSE.getExpectedResponse(sentMessages))); + + // test #4: testServerEndpoint_MultiTextReceived_NoSent + // endpoint: endpoint: void onMessage(Multi message) + sentMessages = new String[] { "When I go", "don't cry for me", "In my Father's arms I'll be", + "The wounds this world left on my soul" }; + streamBuilder.add(Arguments.arguments("received-multi-text-response-none", sentMessages, NO_RESPONSE)); + + // test #5: testServerEndpoint_MultiTextReceived_SingleTextSent + // endpoint: String onMessage(Multi message) + sentMessages = new String[] { "Msg1", "Msg2", "Msg3", "Msg4" }; + streamBuilder.add(Arguments.arguments("received-multi-text-response-single-text", sentMessages, + new String[] { "Alpha Shallows" })); + + // test #6: testServerEndpoint_MultiTextReceived_MultiTextSent + // endpoint: Multi onMessage(Multi message) + sentMessages = new String[] { "Msg1", "Msg2", "Msg3" }; + streamBuilder.add(Arguments.arguments("received-multi-text-response-multi-text", sentMessages, + DOUBLE_ECHO_RESPONSE.getExpectedResponse(sentMessages))); + + // test #7: testServerEndpoint_SingleTextReceived_UniTextSent + // endpoint: Uni onMessage(String message) + sentMessages = new String[] { "Bernie Sanders" }; + streamBuilder.add(Arguments.arguments("received-single-text-response-uni-text", sentMessages, + ECHO_RESPONSE.getExpectedResponse(sentMessages))); + + // test #8: testServerEndpoint_SingleDtoReceived_NoSent + // endpoint: void onMessage(Dto dto) + sentMessages = new String[] { "major disappointment speaking" }; + streamBuilder.add(Arguments.arguments("received-single-dto-response-none", sentMessages, NO_RESPONSE)); + + // test #9: testServerEndpoint_SingleDtoReceived_SingleDtoSent + // endpoint: Dto onMessage(Dto dto) + sentMessages = new String[] { "abcd123456" }; + streamBuilder.add(Arguments.arguments("received-single-dto-response-single-dto", sentMessages, + ECHO_RESPONSE.getExpectedResponse(sentMessages))); + + // test #10: testServerEndpoint_SingleDtoReceived_UniDtoSent + // endpoint: Uni onMessage(Dto dto) + sentMessages = new String[] { "Shot heard round the world" }; + streamBuilder.add(Arguments.arguments("received-single-dto-response-uni-dto", sentMessages, + ECHO_RESPONSE.getExpectedResponse(sentMessages))); + + // test #11: testServerEndpoint_SingleDtoReceived_MultiDtoSent + // endpoint: Multi onMessage(Dto dto) + sentMessages = new String[] { "Bananas are good" }; + streamBuilder.add(Arguments.arguments("received-single-dto-response-multi-dto", sentMessages, + DOUBLE_ECHO_RESPONSE.getExpectedResponse(sentMessages))); + + // test #12: testServerEndpoint_MultiDtoReceived_NoSent + // endpoint: void onMessage(Multi dto) + sentMessages = new String[] { "Tell me how ya livin", "Soljie what ya got givin" }; + streamBuilder.add(Arguments.arguments("received-multi-dto-response-none", sentMessages, NO_RESPONSE)); + + // test #13: testServerEndpoint_MultiDtoReceived_SingleDtoSent + // endpoint: Dto onMessage(Multi message) + sentMessages = new String[] { "Lorem ipsum dolor sit amet", "consectetur adipiscing elit", + "sed do eiusmod tempor incididunt" }; + streamBuilder.add(Arguments.arguments("received-multi-dto-response-single-dto", sentMessages, + new String[] { "ut labore et dolore magna aliqua" })); + + // test #14: testServerEndpoint_MultiDtoReceived_MultiDtoSent + // endpoint: Multi onMessage(Multi dto) + sentMessages = new String[] { "Right", "Left" }; + streamBuilder.add(Arguments.arguments("received-multi-dto-response-multi-dto", sentMessages, + DOUBLE_ECHO_RESPONSE.getExpectedResponse(sentMessages))); + + return streamBuilder.build(); + } + + @Test + public void testClientEndpoint_SingleTextReceived_NoSent() { + var clientConn = bounceClientConnector().baseUri(baseUri).connectAndAwait(); + asserter.serverConnectionOpenedCount += 1; + asserter.clientConnectionOpenedCount += 1; + + var msg1 = "Ut enim ad minim veniam"; + sendClientMessageAndWait(clientConn, msg1); + // 'clientConn' sends 'Ut enim ad minim veniam' + // 'BounceEndpoint' -> 'String onMessage(String message)' sends 'Response 0: Ut enim ad minim veniam' + // 'BounceClient' -> 'void echo(String message)' receives 'Response 0: Ut enim ad minim veniam' + // that is received 2 messages and sent 2 messages + int clientBytesReceived = stringToBytes("echo 0: " + msg1); + int clientBytesSent = stringToBytes(msg1); + int serverBytesReceived = clientBytesSent; + int serverBytesSent = clientBytesReceived; + asserter.assertMetrics(0, 0, 1, serverBytesReceived, serverBytesSent, 1, clientBytesSent, clientBytesReceived); + + msg1 = "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat"; + var msg2 = "Duis aute irure dolor in reprehenderit"; + sendClientMessageAndWait(clientConn, msg1); + sendClientMessageAndWait(clientConn, msg2); + + clientBytesReceived = stringToBytes("echo 0: " + msg1, "echo 0: " + msg2); + clientBytesSent = stringToBytes(msg1, msg2); + serverBytesReceived = clientBytesSent; + serverBytesSent = clientBytesReceived; + asserter.assertMetrics(0, 0, 2, serverBytesReceived, serverBytesSent, 2, clientBytesSent, clientBytesReceived); + + clientConn.closeAndAwait(); + } + + @Test + public void testClientEndpoint_MultiTextReceived_MultiTextSent() { + var clientConn = multiClientConnector().baseUri(baseUri).connectAndAwait(); + asserter.serverConnectionOpenedCount += 1; + asserter.clientConnectionOpenedCount += 1; + + var msg1 = "in voluptate velit esse cillum dolore eu fugiat nulla pariatur"; + var msg2 = "Excepteur sint occaecat cupidatat non proident"; + sendClientMessageAndWait(clientConn, msg1); + sendClientMessageAndWait(clientConn, msg2); + + // 2 sent: 'clientConn' sends 2 messages + // 2 sent, 2 received: 'MultiEndpoint' -> 'Multi echo(Multi messages)' -> accepts and receives message + // 2 sent, 2 received: 'MultiClient' -> 'Multi echo(Multi messages)' -> accepts, receives, adds "echo 0: " + // 2 received: 'MultiEndpoint' -> accepts and returns empty Multi + int clientBytesReceived = stringToBytes(msg1, msg2); + int clientBytesSent = stringToBytes(msg1, msg2, msg1 + "echo 0: ", msg2 + "echo 0: "); + int serverBytesReceived = clientBytesSent; + int serverBytesSent = clientBytesReceived; + + asserter.assertMetrics(0, 0, 4, serverBytesReceived, serverBytesSent, 4, clientBytesSent, clientBytesReceived); + + clientConn.closeAndAwait(); + } + + @Test + public void testServerEndpoint_broadcasting() { + // broadcast = true + // endpoint: String onMessage(String message) + + var msg1 = "It's alright ma"; + // expected metrics: + // endpoint receives msg1 + // 2 connections are opened so 2 responses are expected + int sentBytes = stringToBytes("echo 0: " + msg1, "echo 0: " + msg1); + int receivedBytes = stringToBytes(msg1); + var sentMessages = new String[] { msg1 }; + var connection1 = Connection.of(broadcast_Uri, true, binaryMode(), sentMessages, + ECHO_RESPONSE.getExpectedResponse(sentMessages)); + + var msg2 = "I'm Only Bleeding"; + // expected metrics: + // endpoint receives msg2 + // 2 connections are opened so 2 responses are expected + asserter.serverConnectionOpenedCount += 2; + sentBytes += stringToBytes("echo 0: " + msg2, "echo 0: " + msg2); + receivedBytes += stringToBytes(msg2); + sentMessages = new String[] { msg2 }; + var connection2 = Connection.of(broadcast_Uri, true, binaryMode(), sentMessages, + ECHO_RESPONSE.getExpectedResponse(sentMessages)); + sendAndAssertResponses(vertx, connection1, connection2); + asserter.assertMetrics(0, 2, sentBytes, receivedBytes); + } + + @Test + public void testServerEndpoint_SingleTextReceived_SingleTextSent_MultipleConnections() { + // endpoint: String onMessage(String message) + // testing multiple connections because we need to know that same counter endpoint counter is used by connections + var msg = "Can't Find My Way Home"; + var sentMessages = new String[] { msg }; + var expectedResponses = ECHO_RESPONSE.getExpectedResponse(sentMessages); + + try (var client1 = new WSClient(vertx)) { + client1.connect(new WebSocketConnectOptions(), bounceUri); + var connection1 = Connection.of(bounceUri, false, binaryMode(), sentMessages, expectedResponses); + sendClientMessageAndWait(client1, msg); + asserter.serverConnectionOpenedCount += 1; + asserter.assertMetrics(0, 1, connection1); + + var connection2 = Connection.of(bounceUri, false, binaryMode(), sentMessages, expectedResponses); + sendAndAssertResponses(vertx, connection2); + asserter.serverConnectionOpenedCount += 1; + asserter.assertMetrics(0, 1, connection2); + + var connection3 = Connection.of(bounceUri, false, binaryMode(), sentMessages, expectedResponses); + sendAndAssertResponses(vertx, connection3); + asserter.serverConnectionOpenedCount += 1; + asserter.assertMetrics(0, 1, connection3); + + // --- try different endpoint - start + // endpoint: void onMessage(Multi message) + var sentMessages2 = new String[] { "I get up in the evening", "I ain't nothing but tired", + "I could use just a little help" }; + var connection = Connection.of(multiTextReceived_NoResponse_Uri_2, false, binaryMode(), sentMessages2, NO_RESPONSE); + sendAndAssertResponses(vertx, connection); + asserter.serverConnectionOpenedCount += 1; + asserter.assertMetrics(0, 3, connection); + // --- try different endpoint - end + + var connection4 = Connection.of(bounceUri, false, binaryMode(), sentMessages, expectedResponses); + sendAndAssertResponses(vertx, connection4); + asserter.serverConnectionOpenedCount += 1; + asserter.assertMetrics(0, 1, connection4); + + // send again message via the first connection that is still open + sendClientMessageAndWait(client1, msg); + asserter.assertMetrics(0, 1, connection1); + } + } + + private void sendClientMessageAndWait(WSClient client, String msg) { + if (binaryMode()) { + client.sendAndAwait(Buffer.buffer(msg)); + } else { + client.sendAndAwait(msg); + } + } + + protected void sendClientMessageAndWait(WebSocketClientConnection clientConn, String msg1) { + if (binaryMode()) { + clientConn.sendBinaryAndAwait(Buffer.buffer(msg1)); + } else { + clientConn.sendTextAndAwait(msg1); + } + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/Connection.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/Connection.java new file mode 100644 index 0000000000000..5a6cce46520a8 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/Connection.java @@ -0,0 +1,86 @@ +package io.quarkus.websockets.next.test.telemetry; + +import static io.quarkus.websockets.next.test.utils.WSClient.ReceiverMode.BINARY; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.Set; +import java.util.stream.Collectors; + +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.WebSocketConnectOptions; + +record Connection(URI uri, String[] messagesToSend, WSClient client, boolean broadcast, boolean binaryMode, + String[] expectedResponses) { + + static Connection of(URI uri, boolean broadcast, boolean binaryMode, String[] sentMessages, String[] expectedResponses) { + return new Connection(uri, sentMessages, null, broadcast, binaryMode, expectedResponses); + } + + static Connection of(URI uri, String expectedResponse, boolean binaryMode, String... messages) { + return new Connection(uri, messages, null, false, binaryMode, new String[] { expectedResponse }); + } + + private Connection with(WSClient client) { + return new Connection(uri, messagesToSend, client, broadcast, binaryMode, expectedResponses); + } + + private Set getReceivedMessages() { + return client.getMessages().stream().map(Buffer::toString).collect(Collectors.toSet()); + } + + static void sendAndAssertResponses(Vertx vertx, Connection... connections) { + openConnectionsThenSend(connections, vertx, 0); + } + + private static void openConnectionsThenSend(Connection[] connections, Vertx vertx, int idx) { + var connection = connections[idx]; + final WSClient client = connection.binaryMode() ? new WSClient(vertx, BINARY) : new WSClient(vertx); + try (client) { + client.connect(new WebSocketConnectOptions(), connection.uri()); + connections[idx] = connection.with(client); + + if (idx < connections.length - 1) { + openConnectionsThenSend(connections, vertx, idx + 1); + } else { + sendMessages(connections, connection.binaryMode()); + } + } + } + + private static void sendMessages(Connection[] connections, boolean binaryMode) { + for (Connection connection : connections) { + for (String message : connection.messagesToSend()) { + if (binaryMode) { + connection.client().sendAndAwait(Buffer.buffer(message)); + } else { + connection.client().sendAndAwait(message); + } + } + var expectedResponses = connection.expectedResponses(); + if (expectedResponses.length != 0) { + if (connection.broadcast()) { + for (Connection conn : connections) { + assertResponses(conn, expectedResponses); + } + } else { + assertResponses(connection, expectedResponses); + } + } + } + } + + private static void assertResponses(Connection connection, String[] expectedResponses) { + connection.client.waitForMessages(expectedResponses.length); + Set actualResponses = connection.getReceivedMessages(); + + for (String expectedResponse : expectedResponses) { + assertTrue(actualResponses.contains(expectedResponse), + () -> "Expected response '%s' not found, was: %s".formatted(expectedResponse, actualResponses)); + } + + connection.client().getMessages().clear(); + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/ExpectedServerEndpointResponse.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/ExpectedServerEndpointResponse.java new file mode 100644 index 0000000000000..9f9941d0aaf9e --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/ExpectedServerEndpointResponse.java @@ -0,0 +1,48 @@ +package io.quarkus.websockets.next.test.telemetry; + +import java.util.Arrays; + +public interface ExpectedServerEndpointResponse { + + String[] NO_RESPONSE = new String[] {}; + EchoExpectedServerEndpointResponse ECHO_RESPONSE = new EchoExpectedServerEndpointResponse(); + DoubleEchoExpectedServerEndpointResponse DOUBLE_ECHO_RESPONSE = new DoubleEchoExpectedServerEndpointResponse(); + + /** + * Endpoint returns void, Uni or results in exception and theefore, there is no response. + */ + final class NoExpectedServerEndpointResponse { + + public String[] getExpectedResponse() { + return new String[0]; + } + } + + /** + * Received message is prefixed with 'echo 0: ' and returned. + */ + final class EchoExpectedServerEndpointResponse implements ExpectedServerEndpointResponse { + + public String[] getExpectedResponse(String[] sentMessages) { + return Arrays.stream(sentMessages).map(msg -> "echo 0: " + msg).toArray(String[]::new); + } + + } + + /** + * For each received message 'msg' endpoint returns 'echo 0: msg' and 'echo 1: msg' + */ + final class DoubleEchoExpectedServerEndpointResponse implements ExpectedServerEndpointResponse { + + public String[] getExpectedResponse(String[] sentMessages) { + return Arrays.stream(sentMessages) + .mapMulti((msg, consumer) -> { + consumer.accept("echo 0: " + msg); + consumer.accept("echo 1: " + msg); + }) + .toArray(String[]::new); + } + + } + +} 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 0000000000000..dc4f4d3997c3a --- /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/MetricsAsserter.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MetricsAsserter.java new file mode 100644 index 0000000000000..8cee37c00b427 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MetricsAsserter.java @@ -0,0 +1,238 @@ +package io.quarkus.websockets.next.test.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_MESSAGES_COUNT_ERRORS; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CLIENT_MESSAGES_COUNT_RECEIVED_BYTES; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CLIENT_MESSAGES_COUNT_SENT; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CLIENT_MESSAGES_COUNT_SENT_BYTES; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_CLOSED; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_OPENED; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_OPENED_ERROR; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_MESSAGES_COUNT_ERRORS; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_MESSAGES_COUNT_RECEIVED; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_MESSAGES_COUNT_RECEIVED_BYTES; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_MESSAGES_COUNT_SENT_BYTES; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Arrays; +import java.util.function.Consumer; + +import org.awaitility.Awaitility; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; + +import io.restassured.RestAssured; +import io.restassured.response.ValidatableResponse; + +public final class MetricsAsserter { + + int serverReceivedCount; + int serverReceivedCountBytes; + int serverSentCountBytes; + int clientSentCount; + int clientSentCountBytes; + int clientReceivedCountBytes; + int clientErrorCount; + int serverErrorCount; + int clientConnectionOpenedCount; + int serverConnectionOpenedCount; + + void assertMetrics(int serverErrorsDelta, int serverReceivedCountDelta, Connection connection) { + int serverSentCountBytesDelta = connectionToSentBytes(connection); + int serverReceivedCountBytesDelta = connectionToReceivedBytes(connection); + assertMetrics(serverErrorsDelta, 0, serverReceivedCountDelta, serverReceivedCountBytesDelta, + serverSentCountBytesDelta, 0, 0, 0); + } + + void assertMetrics(int serverErrorsDelta, int serverReceivedCountDelta, int serverSentCountBytesDelta, + int serverReceivedCountBytesDelta) { + assertMetrics(serverErrorsDelta, 0, serverReceivedCountDelta, serverReceivedCountBytesDelta, + serverSentCountBytesDelta, 0, 0, 0); + } + + private int connectionToReceivedBytes(Connection connection) { + return stringToBytes(connection.messagesToSend()); + } + + private int connectionToSentBytes(Connection connection) { + return stringToBytes(connection.expectedResponses()); + } + + void assertMetrics(int serverErrorsDelta, int clientErrorsDelta, int serverReceivedCountDelta, + int serverReceivedCountBytesDelta, int serverSentCountBytesDelta, int clientSentCountDelta, + int clientSentCountBytesDelta, int clientReceivedCountBytesDelta) { + addDeltasToTotalsMeasuredPreviously(serverErrorsDelta, clientErrorsDelta, serverReceivedCountDelta, + serverReceivedCountBytesDelta, serverSentCountBytesDelta, clientSentCountDelta, clientSentCountBytesDelta, + clientReceivedCountBytesDelta); + + assertMetrics(metrics -> metrics + .body(assertServerConnectionOpenedTotal(serverConnectionOpenedCount)) + .body(assertClientConnectionOpenedTotal(clientConnectionOpenedCount)) + .body(assertServerErrorTotal(serverErrorCount)) + .body(assertClientErrorTotal(clientErrorCount)) + .body(assertClientMessagesCountBytesSent(clientSentCountBytes)) + .body(assertClientMessagesCountBytesReceived(clientReceivedCountBytes)) + .body(assertClientMessagesCountSent(clientSentCount)) + .body(assertServerMessagesCountBytesReceived(serverReceivedCountBytes)) + .body(assertServerMessagesCountBytesSent(serverSentCountBytes)) + .body(assertServerMessagesCountReceived(serverReceivedCount))); + } + + private void addDeltasToTotalsMeasuredPreviously(int serverErrorsDelta, int clientErrorsDelta, int serverReceivedCountDelta, + int serverReceivedCountBytesDelta, int serverSentCountBytesDelta, int clientSentCountDelta, + int clientSentCountBytesDelta, int clientReceivedCountBytesDelta) { + serverReceivedCount += serverReceivedCountDelta; + serverReceivedCountBytes += serverReceivedCountBytesDelta; + serverSentCountBytes += serverSentCountBytesDelta; + clientSentCount += clientSentCountDelta; + clientSentCountBytes += clientSentCountBytesDelta; + clientReceivedCountBytes += clientReceivedCountBytesDelta; + clientErrorCount += clientErrorsDelta; + serverErrorCount += serverErrorsDelta; + } + + static Matcher assertClientMessagesCountBytesSent(String path, int clientSentCountBytes) { + return assertTotal(CLIENT_MESSAGES_COUNT_SENT_BYTES, clientSentCountBytes, path); + } + + static Matcher assertClientMessagesCountBytesReceived(String path, int clientReceivedCountBytes) { + return assertTotal(CLIENT_MESSAGES_COUNT_RECEIVED_BYTES, clientReceivedCountBytes, path); + } + + static Matcher assertClientMessagesCountSent(String path, int clientSentCount) { + return assertTotal(CLIENT_MESSAGES_COUNT_SENT, clientSentCount, path); + } + + static Matcher assertServerMessagesCountReceived(String path, int serverReceivedCount) { + return assertTotal(SERVER_MESSAGES_COUNT_RECEIVED, serverReceivedCount, path); + } + + static Matcher assertServerMessagesCountBytesSent(String path, int serverSentCountBytes) { + return assertTotal(SERVER_MESSAGES_COUNT_SENT_BYTES, serverSentCountBytes, path); + } + + static Matcher assertServerMessagesCountBytesReceived(String path, int serverReceivedCountBytes) { + return assertTotal(SERVER_MESSAGES_COUNT_RECEIVED_BYTES, serverReceivedCountBytes, path); + } + + static Matcher assertServerErrorTotal(String path, int serverErrorCount) { + return assertTotal(SERVER_MESSAGES_COUNT_ERRORS, serverErrorCount, path); + } + + static Matcher assertClientErrorTotal(String path, int clientErrorCount) { + return assertTotal(CLIENT_MESSAGES_COUNT_ERRORS, clientErrorCount, path); + } + + static Matcher assertServerConnectionOpeningFailedTotal(String path, int serverConnectionOpeningFailedCount) { + return assertTotal(SERVER_CONNECTION_OPENED_ERROR, serverConnectionOpeningFailedCount, path); + } + + static Matcher assertServerConnectionOpenedTotal(int serverConnectionOpenedCount) { + return assertServerConnectionOpenedTotal(null, serverConnectionOpenedCount); + } + + static Matcher assertClientConnectionOpenedTotal(int clientConnectionOpenedCount) { + return assertClientConnectionOpenedTotal(null, clientConnectionOpenedCount); + } + + static Matcher assertClientMessagesCountBytesSent(int clientSentCountBytes) { + return assertClientMessagesCountBytesSent(null, clientSentCountBytes); + } + + static Matcher assertClientMessagesCountBytesReceived(int clientReceivedCountBytes) { + return assertClientMessagesCountBytesReceived(null, clientReceivedCountBytes); + } + + static Matcher assertClientMessagesCountSent(int clientSentCount) { + return assertClientMessagesCountSent(null, clientSentCount); + } + + static Matcher assertServerMessagesCountReceived(int serverReceivedCount) { + return assertServerMessagesCountReceived(null, serverReceivedCount); + } + + static Matcher assertServerMessagesCountBytesSent(int serverSentCountBytes) { + return assertServerMessagesCountBytesSent(null, serverSentCountBytes); + } + + static Matcher assertServerMessagesCountBytesReceived(int serverReceivedCountBytes) { + return assertServerMessagesCountBytesReceived(null, serverReceivedCountBytes); + } + + static Matcher assertServerErrorTotal(int serverErrorCount) { + return assertServerErrorTotal(null, serverErrorCount); + } + + static Matcher assertClientErrorTotal(int clientErrorCount) { + return assertClientErrorTotal(null, clientErrorCount); + } + + static Matcher assertServerConnectionOpenedTotal(String path, int serverConnectionOpenedCount) { + return assertTotal(SERVER_CONNECTION_OPENED, serverConnectionOpenedCount, path); + } + + static Matcher assertClientConnectionOpenedTotal(String path, int clientConnectionOpenedCount) { + return assertTotal(CLIENT_CONNECTION_OPENED, clientConnectionOpenedCount, path); + } + + static Matcher assertServerConnectionClosedTotal(String path, int serverConnectionClosedCount) { + return assertTotal(SERVER_CONNECTION_CLOSED, serverConnectionClosedCount, path); + } + + static Matcher assertClientConnectionClosedTotal(String path, int clientConnectionClosedCount) { + return assertTotal(CLIENT_CONNECTION_CLOSED, clientConnectionClosedCount, path); + } + + private static Matcher assertTotal(String metricKey, int expectedCount, String path) { + var prometheusFormatKey = "%s_total".formatted(toPrometheusFormat(metricKey)); + return new BaseMatcher<>() { + @Override + public boolean matches(Object o) { + if (o instanceof String str) { + var sameKeyMultipleTags = str + .lines() + .filter(l -> l.contains(prometheusFormatKey)) + .filter(l -> path == null || l.contains(path)) // filter by path + .map(String::trim) + .toList(); + // quarkus_websockets_server_messages_count_received_total{<>} 2.0 + // quarkus_websockets_server_messages_count_received_total{<>} 5.0 + // = 7 + var totalSum = sameKeyMultipleTags + .stream() + .map(l -> l.substring(l.lastIndexOf(" ")).trim()) + .map(Double::parseDouble) + .map(Double::intValue) + .reduce(0, Integer::sum); + return totalSum == expectedCount; + } + return false; + } + + @Override + public void describeTo(Description description) { + description.appendText("Key '%s' with value '%d'".formatted(prometheusFormatKey, expectedCount)); + } + }; + } + + private static String toPrometheusFormat(String dottedMicrometerFormat) { + return dottedMicrometerFormat.replace(".", "_").replace("-", "_"); + } + + private static ValidatableResponse getMetrics() { + return RestAssured.given().get("/q/metrics").then().statusCode(200); + } + + static void assertMetrics(Consumer assertion) { + Awaitility.await().atMost(Duration.ofSeconds(12)).untilAsserted(() -> assertion.accept(getMetrics())); + } + + static int stringToBytes(String... messages) { + return Arrays.stream(messages).map(msg -> msg.getBytes(StandardCharsets.UTF_8)).map(s -> s.length).reduce(0, + Integer::sum); + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MicrometerWebSocketsOnBinaryMessageTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MicrometerWebSocketsOnBinaryMessageTest.java new file mode 100644 index 0000000000000..bb79677de7f43 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MicrometerWebSocketsOnBinaryMessageTest.java @@ -0,0 +1,41 @@ +package io.quarkus.websockets.next.test.telemetry; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.websockets.next.WebSocketConnector; +import io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage.BounceClient; +import io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage.MultiClient; + +/** + * Tests metrics for {@link io.quarkus.websockets.next.OnBinaryMessage}. + */ +public class MicrometerWebSocketsOnBinaryMessageTest extends AbstractWebSocketsOnMessageTest { + + @RegisterExtension + public static final QuarkusUnitTest test = createQuarkusUnitTest( + "io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage"); + + @Inject + WebSocketConnector bounceClientConnector; + + @Inject + WebSocketConnector multiClientConnector; + + @Override + protected boolean binaryMode() { + return true; + } + + @Override + protected WebSocketConnector bounceClientConnector() { + return bounceClientConnector; + } + + @Override + protected WebSocketConnector multiClientConnector() { + return multiClientConnector; + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MicrometerWebSocketsOnErrorTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MicrometerWebSocketsOnErrorTest.java new file mode 100644 index 0000000000000..a4f12734f7eae --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MicrometerWebSocketsOnErrorTest.java @@ -0,0 +1,233 @@ +package io.quarkus.websockets.next.test.telemetry; + +import static io.quarkus.websockets.next.test.telemetry.AbstractWebSocketsOnMessageTest.createQuarkusUnitTest; +import static io.quarkus.websockets.next.test.telemetry.Connection.sendAndAssertResponses; +import static io.quarkus.websockets.next.test.telemetry.ExpectedServerEndpointResponse.ECHO_RESPONSE; +import static io.quarkus.websockets.next.test.telemetry.ExpectedServerEndpointResponse.NO_RESPONSE; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertMetrics; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertServerConnectionOpenedTotal; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertServerConnectionOpeningFailedTotal; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.stringToBytes; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +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.onerror.ErroneousClient_NoOnError; +import io.quarkus.websockets.next.test.telemetry.endpoints.onerror.ErroneousClient_OverloadedOnError; +import io.quarkus.websockets.next.test.telemetry.endpoints.onerror.ErroneousServerEndpoint_OnClose; +import io.quarkus.websockets.next.test.telemetry.endpoints.onerror.ErroneousServerEndpoint_OverriddenOnError; +import io.quarkus.websockets.next.test.telemetry.endpoints.onerror.GlobalErrorHandler; +import io.restassured.RestAssured; +import io.vertx.core.Vertx; + +public class MicrometerWebSocketsOnErrorTest { + + @RegisterExtension + public static final QuarkusUnitTest test = createQuarkusUnitTest( + "io.quarkus.websockets.next.test.telemetry.endpoints.onerror"); + + @Inject + WebSocketConnector erroneousClientConnector_NoOnErr; + + @Inject + WebSocketConnector erroneousClientConnector_OverloadedOnErr; + + @TestHTTPResource("/") + URI baseUri; + + @TestHTTPResource("server-error-no-on-error") + URI serverEndpoint_NoExplicitOnError_Uri; + + @TestHTTPResource("server-error-overridden-on-error") + URI serverEndpoint_OverriddenOnError_Uri; + + @TestHTTPResource("server-error-on-open") + URI serverEndpoint_ErrorOnOpen_Uri; + + @TestHTTPResource("server-error-on-close") + URI serverEndpoint_ErrorOnClose_Uri; + + @TestHTTPResource("server-error-global-handler") + URI serverEndpoint_GlobalErrorHandler_Uri; + + @Inject + Vertx vertx; + + private final MetricsAsserter asserter = new MetricsAsserter(); + + @Test + public void testClientEndpointError_ExceptionInsideOnTextMessage_noExplicitOnErrorDefined() { + // client endpoint doesn't define @OnError + // @OnTextMessage results in a failure + + var clientConn = erroneousClientConnector_NoOnErr.baseUri(baseUri).connectAndAwait(); + asserter.serverConnectionOpenedCount += 1; + asserter.clientConnectionOpenedCount += 1; + var msg = "What'd I Say"; + // 1 sent: use 'clientConn' to send 'msg' + // 1 received, 2 sent: 'ErroneousClientEndpoint_NoOnError' -> 'Multi onMessage(String message)', 2 in Multi + // 2 received: 'ErroneousClient_NoOnError' -> 'Uni onMessage(String message)' + int clientBytesSent = stringToBytes(msg); + int clientBytesReceived = stringToBytes("echo 0: " + msg, "echo 1: " + msg); + int serverBytesReceived = clientBytesSent; + int serverBytesSent = clientBytesReceived; + + clientConn.sendTextAndAwait(msg); + Awaitility.await().untilAsserted(() -> Assertions.assertEquals(2, ErroneousClient_NoOnError.MESSAGES.size())); + asserter.assertMetrics(0, 0, 1, serverBytesReceived, serverBytesSent, 1, clientBytesSent, clientBytesReceived); + + // 'ErroneousClient_NoOnError' throws exception inside 'onMessage' after it received 4 messages + clientConn.sendTextAndAwait(msg); + Awaitility.await().untilAsserted(() -> Assertions.assertEquals(4, ErroneousClient_NoOnError.MESSAGES.size())); + asserter.assertMetrics(0, 1, 1, serverBytesReceived, serverBytesSent, 1, clientBytesSent, clientBytesReceived); + + clientConn.closeAndAwait(); + } + + @Test + public void testClientEndpointError_ExceptionInsideOnTextMessage_WithOverloadedOnError() throws InterruptedException { + // client endpoint defines multiple @OnError + // @OnTextMessage results in a failure + + var clientConn = erroneousClientConnector_OverloadedOnErr.baseUri(baseUri).connectAndAwait(); + asserter.serverConnectionOpenedCount += 1; + asserter.clientConnectionOpenedCount += 1; + var msg = "What'd I Say"; + int clientBytesSent = stringToBytes(msg); + int clientBytesReceived = stringToBytes("echo 0: " + msg, "echo 1: " + msg); + int serverBytesReceived = clientBytesSent; + int serverBytesSent = clientBytesReceived; + + // 1 sent: use 'clientConn' to send 'msg' + // 1 received, 2 sent: 'ErroneousClientEndpoint_OverloadedOnError' -> 'Multi onMessage(String message)' + // 2 received: 'ErroneousClient_OverloadedOnError' -> 'Uni onMessage(String message)' + clientConn.sendTextAndAwait(msg); + + // assert messages and metrics + Awaitility.await().untilAsserted(() -> Assertions.assertEquals(2, ErroneousClient_OverloadedOnError.MESSAGES.size())); + // assert no client exception collected as metric + asserter.assertMetrics(0, 0, 1, serverBytesReceived, serverBytesSent, 1, clientBytesSent, clientBytesReceived); + + // 1 sent: use 'clientConn' to send 'msg' + // 1 received, 2 sent: 'ErroneousClientEndpoint_OverloadedOnError' -> 'Multi onMessage(String message)' + // 2 received: 'ErroneousClient_OverloadedOnError' -> 'Uni onMessage(String message)' + // after 4 messages, a RuntimeException is thrown + // 1 sent: 'ErroneousClient_OverloadedOnError' recovers with 'recoveryMsg' in + // @OnError 'String onError(RuntimeException e)' + // 1 received, 2 sent: 'ErroneousClientEndpoint_OverloadedOnError' (that 'recoveryMsg') + // 2 received: in 'ErroneousClient_OverloadedOnError' + // === total expected: 6 received, 6 sent + clientConn.sendTextAndAwait(msg); + + // client @OnError returns this String which is sent to the server @OnMessage, so expect extra bytes + var recoveryMsg = "Expected error - 4 items"; + int extraClientSentBytes = stringToBytes(recoveryMsg); + int extraServerReceivedBytes = extraClientSentBytes; + int extraServerSentBytes = stringToBytes("echo 0: " + recoveryMsg, "echo 1: " + recoveryMsg); + int extraClientReceivedBytes = extraServerSentBytes; + + // assert messages and metrics + Awaitility.await().untilAsserted(() -> Assertions.assertEquals(6, ErroneousClient_OverloadedOnError.MESSAGES.size())); + assertTrue(ErroneousClient_OverloadedOnError.RUNTIME_EXCEPTION_LATCH.await(2, TimeUnit.SECONDS)); + asserter.assertMetrics(0, 1, 2, serverBytesReceived + extraServerReceivedBytes, serverBytesSent + extraServerSentBytes, + 2, clientBytesSent + extraClientSentBytes, clientBytesReceived + extraClientReceivedBytes); + + // 1 sent: use 'clientConn' to send 'msg' + // 1 received, 2 sent: 'ErroneousClientEndpoint_OverloadedOnError' -> 'Multi onMessage(String message)' + // 2 received: 'ErroneousClient_OverloadedOnError' -> 'Uni onMessage(String message)' + clientConn.sendTextAndAwait(msg); + + // assert messages and metrics + Awaitility.await().untilAsserted(() -> Assertions.assertEquals(8, ErroneousClient_OverloadedOnError.MESSAGES.size())); + // after 8 messages, an IllegalStateException is thrown + // @OnError void onError(IllegalStateException e) + assertTrue(ErroneousClient_OverloadedOnError.ILLEGAL_STATE_EXCEPTION_LATCH.await(2, TimeUnit.SECONDS)); + asserter.assertMetrics(0, 1, 1, serverBytesReceived, serverBytesSent, 1, clientBytesSent, clientBytesReceived); + + clientConn.closeAndAwait(); + } + + @Test + public void testServerEndpointError_ExceptionDuringTextDecoding_noExplicitOnErrorDefined() { + // server endpoint @OnTextMessage: Uni onMessage(Multi dto) + // text codec throws exception + // no explicit @OnError + var connection = Connection.of(serverEndpoint_NoExplicitOnError_Uri, false, false, new String[] { "Billions" }, + NO_RESPONSE); + asserter.serverConnectionOpenedCount += 1; + sendAndAssertResponses(vertx, connection); + asserter.assertMetrics(1, 1, connection); + } + + @Test + public void testServerEndpointError_ExceptionDuringBinaryDecoding_OnErrorOverloaded() throws InterruptedException { + // server endpoint @OnBinaryMessage: Uni onMessage(Multi dto) + // @OnError: void onError(RuntimeException e) + var msg = "Wendy"; + var connection = Connection.of(serverEndpoint_OverriddenOnError_Uri, false, true, new String[] { msg }, NO_RESPONSE); + asserter.serverConnectionOpenedCount += 1; + sendAndAssertResponses(vertx, connection); + assertTrue(ErroneousServerEndpoint_OverriddenOnError.RUNTIME_EXCEPTION_LATCH.await(5, TimeUnit.SECONDS)); + asserter.assertMetrics(1, 1, connection); + } + + @Test + public void testServerEndpointError_ExceptionInsideOnOpen() { + // error happens in @OnOpen, @OnTextMessage is invoked but the connection is already closed + var connection = Connection.of(serverEndpoint_ErrorOnOpen_Uri, false, false, new String[] { "Rhodes" }, NO_RESPONSE); + asserter.serverConnectionOpenedCount += 1; + sendAndAssertResponses(vertx, connection); + asserter.assertMetrics(1, 1, connection); + } + + @Test + public void testServerEndpointError_ExceptionInsideOnClose() throws InterruptedException { + // @OnBinaryMessage is called: Multi onMessage(String message) + // expect 1 received message and one response + // @OnClose fails with IllegalStateException + // explicitly declared @OnError catches the exception + var connection = Connection.of(serverEndpoint_ErrorOnClose_Uri, "Bobby", true, "Chuck"); + asserter.serverConnectionOpenedCount += 1; + sendAndAssertResponses(vertx, connection); + assertTrue(ErroneousServerEndpoint_OnClose.ILLEGAL_STATE_EXCEPTION_LATCH.await(7, TimeUnit.SECONDS)); + asserter.assertMetrics(1, 1, connection); + } + + @Test + public void testServerEndpointError_GlobalErrorHandler() throws InterruptedException { + // test that error handled by a global error handler (defined outside the endpoint) are accounted for + // global error handler recovers exception with original message: String onError(IllegalArgumentException e) + // we need to check that both error and response sent from the global handler (bytes) are collected as a metric + var sentMessages = new String[] { "Hold the Line" }; + var connection = Connection.of(serverEndpoint_GlobalErrorHandler_Uri, false, false, sentMessages, + ECHO_RESPONSE.getExpectedResponse(sentMessages)); + asserter.serverConnectionOpenedCount += 1; + sendAndAssertResponses(vertx, connection); + assertTrue(GlobalErrorHandler.ILLEGAL_ARGUMENT_EXCEPTION_LATCH.await(5, TimeUnit.SECONDS)); + // on each message, an exception is raised, we send 1 message -> expect 1 error + asserter.assertMetrics(1, 1, connection); + } + + @Test + public void testServerEndpoint_HttpUpgradeFailed() { + // opening WebSocket connection failed and we need it tracked + var path = "/http-upgrade-failed"; + asserter.serverConnectionOpenedCount += 1; + RestAssured.given().get(path).then().statusCode(400); + assertMetrics(m -> m + .body(assertServerConnectionOpenedTotal(asserter.serverConnectionOpenedCount)) + .body(assertServerConnectionOpenedTotal(path, 1)) + .body(assertServerConnectionOpeningFailedTotal(path, 1))); + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MicrometerWebSocketsOnTextMessageTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MicrometerWebSocketsOnTextMessageTest.java new file mode 100644 index 0000000000000..52122f9308d59 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/MicrometerWebSocketsOnTextMessageTest.java @@ -0,0 +1,163 @@ +package io.quarkus.websockets.next.test.telemetry; + +import static io.quarkus.websockets.next.test.telemetry.Connection.sendAndAssertResponses; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertClientConnectionClosedTotal; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertClientConnectionOpenedTotal; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertClientMessagesCountBytesReceived; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertClientMessagesCountBytesSent; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertClientMessagesCountSent; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertMetrics; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertServerConnectionClosedTotal; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertServerConnectionOpenedTotal; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertServerMessagesCountBytesReceived; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertServerMessagesCountBytesSent; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.assertServerMessagesCountReceived; +import static io.quarkus.websockets.next.test.telemetry.MetricsAsserter.stringToBytes; + +import java.net.URI; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +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.ClientEndpointWithPathParams; +import io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage.MultiClient; + +/** + * Tests metrics for {@link io.quarkus.websockets.next.OnTextMessage}. + */ +public class MicrometerWebSocketsOnTextMessageTest extends AbstractWebSocketsOnMessageTest { + + @RegisterExtension + public static final QuarkusUnitTest test = createQuarkusUnitTest( + "io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage"); + + @Inject + WebSocketConnector bounceClientConnector; + + @Inject + WebSocketConnector multiClientConnector; + + @Inject + WebSocketConnector clientWithPathParamsConnector; + + @TestHTTPResource("/ping/ho/and/hey") + URI testServerPathParam1; + + @TestHTTPResource("/ping/amy/and/macdonald") + URI testServerPathParam2; + + @Override + protected boolean binaryMode() { + return false; + } + + @Override + protected WebSocketConnector bounceClientConnector() { + return bounceClientConnector; + } + + @Override + protected WebSocketConnector multiClientConnector() { + return multiClientConnector; + } + + @Test + public void testServerEndpoint_PathParams_ResponseFromOnOpenMethod() { + // endpoint: @OnOpen String process(@PathParam String one, @PathParam String two) + // path: /ping/{one}/and/{two} -> one:two + var path = "/ping/:one/and/:two"; + var expectedResponse = "ho:hey"; // path is /ping/ho/and/hey + var connection1 = Connection.of(testServerPathParam1, expectedResponse, binaryMode(), "whatever"); + asserter.serverConnectionOpenedCount += 1; + sendAndAssertResponses(vertx, connection1); + + // assert totals for all the path tags + asserter.assertMetrics(0, 1, connection1); + // assert metric for our path tag only (this is sent from @OnOpen) + int serverBytesSent1 = stringToBytes(expectedResponse); + assertMetrics(metrics -> metrics.body(assertServerMessagesCountBytesSent(path, serverBytesSent1))); + + var expectedResponse2 = "amy:macdonald"; // path is /ping/amy/and/macdonald + var connection2 = Connection.of(testServerPathParam2, expectedResponse2, binaryMode(), "whatever"); + asserter.serverConnectionOpenedCount += 1; + sendAndAssertResponses(vertx, connection2); + + // assert totals for all the path tags + asserter.assertMetrics(0, 1, connection2); + // assert metric for our path tag only (this is sent from @OnOpen) (no deltas, so previous bytes + current ones) + int serverBytesSent2 = stringToBytes(expectedResponse2); + assertMetrics(metrics -> metrics.body(assertServerMessagesCountBytesSent(path, serverBytesSent1 + serverBytesSent2))); + } + + @Test + public void testClientEndpoint_PathParam() { + // server endpoint: Uni onMessage(String message) + // client endpoint: void onTextMessage(String message) + var msg = "Ut enim ad minim veniam"; + + var clientConn = clientWithPathParamsConnector + .baseUri(baseUri) + .pathParam("name", "Lu=") + .connectAndAwait(); + asserter.serverConnectionOpenedCount += 1; + asserter.clientConnectionOpenedCount += 1; + sendClientMessageAndWait(clientConn, msg); + // 'clientConn' sends 'Ut enim ad minim veniam' + // server endpoint - 1 received, 1 sent: Uni onMessage(String message) + // client endpoint - 1 received: void onTextMessage(String message) + // that is received 2 messages and sent 2 messages + int clientBytesReceived = stringToBytes("echo 0: " + msg); + int clientBytesSent = stringToBytes(msg); + int serverBytesReceived = clientBytesSent; + int serverBytesSent = clientBytesReceived; + + // assert totals for all the path tags + asserter.assertMetrics(0, 0, 1, serverBytesReceived, serverBytesSent, 1, clientBytesSent, clientBytesReceived); + // assert metric for our path tag only + clientConn.closeAndAwait(); + assertMetrics(metrics -> metrics + .body(assertClientConnectionClosedTotal("/client-endpoint-with-path-param/{name}", 1)) + .body(assertClientConnectionOpenedTotal("/client-endpoint-with-path-param/{name}", 1)) + .body(assertClientMessagesCountBytesSent("/client-endpoint-with-path-param/{name}", clientBytesSent)) + .body(assertClientMessagesCountSent("/client-endpoint-with-path-param/{name}", 1)) + .body(assertClientMessagesCountBytesReceived("/client-endpoint-with-path-param/{name}", clientBytesReceived)) + .body(assertServerConnectionClosedTotal("/client-endpoint-with-path-param/:name", 1)) + .body(assertServerMessagesCountReceived("/client-endpoint-with-path-param/:name", 1)) + .body(assertServerConnectionOpenedTotal("/client-endpoint-with-path-param/:name", 1)) + .body(assertServerMessagesCountBytesReceived("/client-endpoint-with-path-param/:name", serverBytesReceived)) + .body(assertServerMessagesCountBytesSent("/client-endpoint-with-path-param/:name", serverBytesSent))); + + clientConn = clientWithPathParamsConnector + .baseUri(baseUri) + .pathParam("name", "Go=Through") + .connectAndAwait(); + asserter.serverConnectionOpenedCount += 1; + asserter.clientConnectionOpenedCount += 1; + sendClientMessageAndWait(clientConn, msg); + + // assert totals for all the path tags + asserter.assertMetrics(0, 0, 1, serverBytesReceived, serverBytesSent, 1, clientBytesSent, clientBytesReceived); + // assert metric for our path tag only (prev + current ones, no deltas here) + clientConn.closeAndAwait(); + assertMetrics(metrics -> metrics + .body(assertClientConnectionOpenedTotal("/client-endpoint-with-path-param/{name}", 2)) + .body(assertClientConnectionClosedTotal("/client-endpoint-with-path-param/{name}", 2)) + .body(assertClientMessagesCountBytesSent("/client-endpoint-with-path-param/{name}", clientBytesSent * 2)) + .body(assertClientMessagesCountSent("/client-endpoint-with-path-param/{name}", 2)) + .body(assertClientMessagesCountBytesReceived("/client-endpoint-with-path-param/{name}", + clientBytesReceived * 2)) + .body(assertServerConnectionOpenedTotal("/client-endpoint-with-path-param/:name", 2)) + .body(assertServerConnectionClosedTotal("/client-endpoint-with-path-param/:name", 2)) + .body(assertServerMessagesCountReceived("/client-endpoint-with-path-param/:name", 2)) + .body(assertServerMessagesCountBytesReceived("/client-endpoint-with-path-param/:name", serverBytesReceived * 2)) + .body(assertServerMessagesCountBytesSent("/client-endpoint-with-path-param/:name", serverBytesSent * 2))); + + } + +} 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 0000000000000..419ab83963980 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/OpenTelemetryWebSocketsTest.java @@ -0,0 +1,202 @@ +package io.quarkus.websockets.next.test.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.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 io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_CLOSED; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_OPENED; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.URI_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.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); + assertServerTraces(); + } + + @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); + assertServerTraces(); + assertClientTraces(); + } + + @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); + assertServerTraces(); + } + + private void assertClientTraces() { + var connectionOpenedSpan = getSpanByName(CLIENT_CONNECTION_OPENED); + assertEquals(bounceUri.getPath(), getUriAttrVal(connectionOpenedSpan)); + assertTrue(connectionOpenedSpan.getLinks().isEmpty()); + + var connectionClosedSpan = getSpanByName(CLIENT_CONNECTION_CLOSED); + 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()); + } + + private void assertServerTraces() { + var initialRequestSpan = getSpanByName("GET /bounce"); + + var connectionOpenedSpan = getSpanByName(SERVER_CONNECTION_OPENED); + assertEquals(bounceUri.getPath(), getUriAttrVal(connectionOpenedSpan)); + assertEquals(initialRequestSpan.getSpanId(), connectionOpenedSpan.getLinks().get(0).getSpanContext().getSpanId()); + + var connectionClosedSpan = getSpanByName(SERVER_CONNECTION_CLOSED); + 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(AttributeKey.stringKey(URI_ATTR_KEY)); + } + + 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(30)) + .untilAsserted(() -> assertEquals(expectedTracesCount, spanExporter.getFinishedSpanItems().size())); + } + + private SpanData getSpanByName(String name) { + return spanExporter.getFinishedSpanItems() + .stream() + .filter(sd -> name.equals(sd.getName())) + .findFirst() + .orElseThrow(() -> new AssertionError( + "Expected span '" + name + "' not found: " + spanExporter.getFinishedSpanItems())); + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/BounceClient.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/BounceClient.java new file mode 100644 index 0000000000000..b4ade79356a61 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/BounceClient.java @@ -0,0 +1,13 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocketClient; + +@WebSocketClient(path = "/bounce") +public class BounceClient { + + @OnBinaryMessage + void echo(String message) { + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/BounceEndpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/BounceEndpoint.java new file mode 100644 index 0000000000000..a450e35dc75c9 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/BounceEndpoint.java @@ -0,0 +1,14 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/bounce") +public class BounceEndpoint { + + @OnBinaryMessage + public String onMessage(String message) { + return "echo 0: " + message; + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/BroadcastingEndpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/BroadcastingEndpoint.java new file mode 100644 index 0000000000000..937a4b4cc3855 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/BroadcastingEndpoint.java @@ -0,0 +1,14 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/broadcast") +public class BroadcastingEndpoint { + + @OnBinaryMessage(broadcast = true) + public String onMessage(String message) { + return "echo 0: " + message; + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/Dto.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/Dto.java new file mode 100644 index 0000000000000..a23f0010af32b --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/Dto.java @@ -0,0 +1,5 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +public record Dto(String property) { + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/DtoBinaryCodec.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/DtoBinaryCodec.java new file mode 100644 index 0000000000000..59606220cc838 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/DtoBinaryCodec.java @@ -0,0 +1,29 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import java.lang.reflect.Type; + +import jakarta.annotation.Priority; +import jakarta.inject.Singleton; + +import io.quarkus.websockets.next.BinaryMessageCodec; +import io.vertx.core.buffer.Buffer; + +@Priority(15) +@Singleton +public class DtoBinaryCodec implements BinaryMessageCodec { + @Override + public boolean supports(Type type) { + return type.equals(Dto.class); + } + + @Override + public Buffer encode(Dto dto) { + return Buffer.buffer(dto.property()); + } + + @Override + public Dto decode(Type type, Buffer value) { + return new Dto(value.toString()); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiClient.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiClient.java new file mode 100644 index 0000000000000..7838101c3e365 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiClient.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocketClient; +import io.smallrye.mutiny.Multi; + +@WebSocketClient(path = "/multi") +public class MultiClient { + + @OnBinaryMessage + Multi echo(Multi messages) { + return messages.map(msg -> "echo 0: " + msg); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiDtoReceived_MultiDtoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiDtoReceived_MultiDtoResponse_Endpoint.java new file mode 100644 index 0000000000000..5749796189ae7 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiDtoReceived_MultiDtoResponse_Endpoint.java @@ -0,0 +1,18 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-multi-dto-response-multi-dto") +public class MultiDtoReceived_MultiDtoResponse_Endpoint { + + @OnBinaryMessage + public Multi onMessage(Multi messages) { + return messages + .map(Dto::property) + .flatMap(msg -> Multi.createFrom().items("echo 0: " + msg, "echo 1: " + msg)) + .map(Dto::new); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiDtoReceived_NoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiDtoReceived_NoResponse_Endpoint.java new file mode 100644 index 0000000000000..40b0efd7bcc3f --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiDtoReceived_NoResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-multi-dto-response-none") +public class MultiDtoReceived_NoResponse_Endpoint { + + @OnBinaryMessage + public void onMessage(Multi dto) { + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiDtoReceived_SingleDtoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiDtoReceived_SingleDtoResponse_Endpoint.java new file mode 100644 index 0000000000000..41a424ccacd14 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiDtoReceived_SingleDtoResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-multi-dto-response-single-dto") +public class MultiDtoReceived_SingleDtoResponse_Endpoint { + + @OnBinaryMessage + public String onMessage(Multi message) { + return "ut labore et dolore magna aliqua"; + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiEndpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiEndpoint.java new file mode 100644 index 0000000000000..2effe004f782b --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiEndpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/multi") +public class MultiEndpoint { + + @OnBinaryMessage + Multi echo(Multi messages) { + return messages.filter(msg -> !msg.startsWith("echo 0: ")); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiTextReceived_MultiTextResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiTextReceived_MultiTextResponse_Endpoint.java new file mode 100644 index 0000000000000..5e3e050676052 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiTextReceived_MultiTextResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-multi-text-response-multi-text") +public class MultiTextReceived_MultiTextResponse_Endpoint { + + @OnBinaryMessage + public Multi onMessage(Multi messages) { + return messages.flatMap(msg -> Multi.createFrom().items("echo 0: " + msg, "echo 1: " + msg)); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiTextReceived_NoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiTextReceived_NoResponse_Endpoint.java new file mode 100644 index 0000000000000..1c696acc26287 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiTextReceived_NoResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-multi-text-response-none") +public class MultiTextReceived_NoResponse_Endpoint { + + @OnBinaryMessage + public void onMessage(Multi message) { + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiTextReceived_NoResponse_Endpoint_2.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiTextReceived_NoResponse_Endpoint_2.java new file mode 100644 index 0000000000000..83a3053ec840e --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiTextReceived_NoResponse_Endpoint_2.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-multi-text-response-none-2") +public class MultiTextReceived_NoResponse_Endpoint_2 { + + @OnBinaryMessage + public void onMessage(Multi message) { + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiTextReceived_SingleTextResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiTextReceived_SingleTextResponse_Endpoint.java new file mode 100644 index 0000000000000..e3c4acb81aba0 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/MultiTextReceived_SingleTextResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-multi-text-response-single-text") +public class MultiTextReceived_SingleTextResponse_Endpoint { + + @OnBinaryMessage + public String onMessage(Multi message) { + return "Alpha Shallows"; + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleDtoReceived_MultiDtoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleDtoReceived_MultiDtoResponse_Endpoint.java new file mode 100644 index 0000000000000..c7dc340a24dca --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleDtoReceived_MultiDtoResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-single-dto-response-multi-dto") +public class SingleDtoReceived_MultiDtoResponse_Endpoint { + + @OnBinaryMessage + public Multi onMessage(Dto dto) { + return Multi.createFrom().items("echo 0: " + dto.property(), "echo 1: " + dto.property()).map(Dto::new); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleDtoReceived_NoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleDtoReceived_NoResponse_Endpoint.java new file mode 100644 index 0000000000000..32dfe6c55ea0f --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleDtoReceived_NoResponse_Endpoint.java @@ -0,0 +1,13 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/received-single-dto-response-none") +public class SingleDtoReceived_NoResponse_Endpoint { + + @OnBinaryMessage + public void onMessage(Dto dto) { + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleDtoReceived_SingleDtoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleDtoReceived_SingleDtoResponse_Endpoint.java new file mode 100644 index 0000000000000..53c3350eb94f4 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleDtoReceived_SingleDtoResponse_Endpoint.java @@ -0,0 +1,14 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/received-single-dto-response-single-dto") +public class SingleDtoReceived_SingleDtoResponse_Endpoint { + + @OnBinaryMessage + public Dto onMessage(Dto dto) { + return new Dto("echo 0: " + dto.property()); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleDtoReceived_UniDtoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleDtoReceived_UniDtoResponse_Endpoint.java new file mode 100644 index 0000000000000..50ab704fa13a9 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleDtoReceived_UniDtoResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Uni; + +@WebSocket(path = "/received-single-dto-response-uni-dto") +public class SingleDtoReceived_UniDtoResponse_Endpoint { + + @OnBinaryMessage + public Uni onMessage(Dto dto) { + return Uni.createFrom().item("echo 0: " + dto.property()).map(Dto::new); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleTextReceived_MultiTextResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleTextReceived_MultiTextResponse_Endpoint.java new file mode 100644 index 0000000000000..16f20c5b121d8 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleTextReceived_MultiTextResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-single-text-response-multi-text") +public class SingleTextReceived_MultiTextResponse_Endpoint { + + @OnBinaryMessage + public Multi onMessage(String message) { + return Multi.createFrom().items("echo 0: " + message, "echo 1: " + message); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleTextReceived_NoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleTextReceived_NoResponse_Endpoint.java new file mode 100644 index 0000000000000..661fbf710af12 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleTextReceived_NoResponse_Endpoint.java @@ -0,0 +1,14 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/received-single-text-response-none") +public class SingleTextReceived_NoResponse_Endpoint { + + @OnBinaryMessage + public void onMessage(String message) { + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleTextReceived_SingleTextSent_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleTextReceived_SingleTextSent_Endpoint.java new file mode 100644 index 0000000000000..d2bd81f7a2dda --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleTextReceived_SingleTextSent_Endpoint.java @@ -0,0 +1,14 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/single-text-received-single-text-sent") +public class SingleTextReceived_SingleTextSent_Endpoint { + + @OnBinaryMessage + public String onMessage(String message) { + return "echo 0: " + message; + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleTextReceived_UniTextResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleTextReceived_UniTextResponse_Endpoint.java new file mode 100644 index 0000000000000..9ca22e0549315 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onbinarymessage/SingleTextReceived_UniTextResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onbinarymessage; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Uni; + +@WebSocket(path = "/received-single-text-response-uni-text") +public class SingleTextReceived_UniTextResponse_Endpoint { + + @OnBinaryMessage + public Uni onMessage(String message) { + return Uni.createFrom().item("echo 0: " + message); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/Dto.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/Dto.java new file mode 100644 index 0000000000000..0f1f22f218034 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/Dto.java @@ -0,0 +1,5 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onerror; + +public record Dto(String property) { + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/DtoBinaryCodec.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/DtoBinaryCodec.java new file mode 100644 index 0000000000000..a1aadb33dd685 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/DtoBinaryCodec.java @@ -0,0 +1,30 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onerror; + +import java.lang.reflect.Type; + +import jakarta.annotation.Priority; +import jakarta.inject.Singleton; + +import io.quarkus.websockets.next.BinaryMessageCodec; +import io.vertx.core.buffer.Buffer; + +@Priority(15) +@Singleton +public class DtoBinaryCodec + implements BinaryMessageCodec { + @Override + public boolean supports(Type type) { + return type.equals(Dto.class); + } + + @Override + public Buffer encode(Dto dto) { + return Buffer.buffer(dto.property()); + } + + @Override + public Dto decode(Type type, Buffer value) { + throw new RuntimeException("Expected exception during decoding"); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/DtoTextCodec.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/DtoTextCodec.java new file mode 100644 index 0000000000000..0cf147961269a --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/DtoTextCodec.java @@ -0,0 +1,27 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onerror; + +import java.lang.reflect.Type; + +import jakarta.annotation.Priority; +import jakarta.inject.Singleton; + +import io.quarkus.websockets.next.TextMessageCodec; + +@Priority(15) // this must have higher priority than JsonCodec or tests will be flaky +@Singleton +public class DtoTextCodec implements TextMessageCodec { + @Override + public boolean supports(Type type) { + return type.equals(Dto.class); + } + + @Override + public String encode(Dto dto) { + return dto.property(); + } + + @Override + public Dto decode(Type type, String value) { + throw new RuntimeException("Expected exception during decoding"); + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousClientEndpoint_NoOnError.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousClientEndpoint_NoOnError.java new file mode 100644 index 0000000000000..f833049d45e5c --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousClientEndpoint_NoOnError.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onerror; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/client-error-no-on-error") +public class ErroneousClientEndpoint_NoOnError { + + @OnTextMessage + public Multi onMessage(String message) { + return Multi.createFrom().items("echo 0: " + message, "echo 1: " + message); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousClientEndpoint_OverloadedOnError.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousClientEndpoint_OverloadedOnError.java new file mode 100644 index 0000000000000..bee63ca60ab6f --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousClientEndpoint_OverloadedOnError.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onerror; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/client-error-overloaded-on-error") +public class ErroneousClientEndpoint_OverloadedOnError { + + @OnTextMessage + public Multi onMessage(String message) { + return Multi.createFrom().items("echo 0: " + message, "echo 1: " + message); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousClient_NoOnError.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousClient_NoOnError.java new file mode 100644 index 0000000000000..9ed6f0d365df1 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousClient_NoOnError.java @@ -0,0 +1,26 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onerror; + +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocketClient; +import io.smallrye.mutiny.Uni; + +@WebSocketClient(path = "/client-error-no-on-error") +public class ErroneousClient_NoOnError { + + public static List MESSAGES = new ArrayList<>(); + + @OnTextMessage + Uni onMessage(String message) { + synchronized (this) { + MESSAGES.add(message); + if (MESSAGES.size() == 4) { + return Uni.createFrom().failure(new RuntimeException("You asked for an error, you got the error!")); + } + return Uni.createFrom().voidItem(); + } + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousClient_OverloadedOnError.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousClient_OverloadedOnError.java new file mode 100644 index 0000000000000..2a7c4f99b6a6b --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousClient_OverloadedOnError.java @@ -0,0 +1,43 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onerror; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocketClient; +import io.smallrye.mutiny.Uni; + +@WebSocketClient(path = "/client-error-overloaded-on-error") +public class ErroneousClient_OverloadedOnError { + + public static CountDownLatch RUNTIME_EXCEPTION_LATCH = new CountDownLatch(1); + public static CountDownLatch ILLEGAL_STATE_EXCEPTION_LATCH = new CountDownLatch(1); + public static List MESSAGES = new ArrayList<>(); + + @OnTextMessage + Uni onMessage(String message) { + synchronized (this) { + MESSAGES.add(message); + if (MESSAGES.size() == 4) { + return Uni.createFrom().failure(new RuntimeException("Expected error - 4 items")); + } + if (MESSAGES.size() == 8) { + return Uni.createFrom().failure(new IllegalStateException("Expected error - 8 items")); + } + return Uni.createFrom().voidItem(); + } + } + + @OnError + public String onError(RuntimeException e) { + RUNTIME_EXCEPTION_LATCH.countDown(); + return e.getMessage(); + } + + @OnError + public void onError(IllegalStateException e) { + ILLEGAL_STATE_EXCEPTION_LATCH.countDown(); + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousEndpoint_GlobalErrorHandler.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousEndpoint_GlobalErrorHandler.java new file mode 100644 index 0000000000000..65d5cb9a1523f --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousEndpoint_GlobalErrorHandler.java @@ -0,0 +1,19 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onerror; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Uni; + +@WebSocket(path = "/server-error-global-handler") +public class ErroneousEndpoint_GlobalErrorHandler { + + private final AtomicInteger counter = new AtomicInteger(); + + @OnTextMessage + public Uni onMessage(String txt) { + return Uni.createFrom().failure(new IllegalArgumentException("echo " + counter.getAndIncrement() + ": " + txt)); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousServerEndpoint_NoOnError.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousServerEndpoint_NoOnError.java new file mode 100644 index 0000000000000..700732956c6f5 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousServerEndpoint_NoOnError.java @@ -0,0 +1,16 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onerror; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +@WebSocket(path = "/server-error-no-on-error") +public class ErroneousServerEndpoint_NoOnError { + + @OnTextMessage + public Uni onMessage(Multi dto) { + return dto.toUni(); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousServerEndpoint_OnClose.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousServerEndpoint_OnClose.java new file mode 100644 index 0000000000000..193051558a100 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousServerEndpoint_OnClose.java @@ -0,0 +1,30 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onerror; + +import java.util.concurrent.CountDownLatch; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.OnClose; +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/server-error-on-close") +public class ErroneousServerEndpoint_OnClose { + + public static CountDownLatch ILLEGAL_STATE_EXCEPTION_LATCH = new CountDownLatch(1); + + @OnBinaryMessage + public Multi onMessage(String message) { + return Multi.createFrom().items("Bobby"); + } + + @OnClose + public void onClose() { + throw new IllegalStateException("Expected exception"); + } + + @OnError + public void onError(IllegalStateException e) { + ILLEGAL_STATE_EXCEPTION_LATCH.countDown(); + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousServerEndpoint_OnOpen.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousServerEndpoint_OnOpen.java new file mode 100644 index 0000000000000..685db5069f47d --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousServerEndpoint_OnOpen.java @@ -0,0 +1,20 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onerror; + +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Uni; + +@WebSocket(path = "/server-error-on-open") +public class ErroneousServerEndpoint_OnOpen { + + @OnOpen + public Uni onOpen() { + return Uni.createFrom().failure(new IllegalStateException("Expected failure")); + } + + @OnTextMessage + public void onMessage(String message) { + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousServerEndpoint_OverriddenOnError.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousServerEndpoint_OverriddenOnError.java new file mode 100644 index 0000000000000..2dbc194f31639 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/ErroneousServerEndpoint_OverriddenOnError.java @@ -0,0 +1,25 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onerror; + +import java.util.concurrent.CountDownLatch; + +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +@WebSocket(path = "/server-error-overridden-on-error") +public class ErroneousServerEndpoint_OverriddenOnError { + + public static CountDownLatch RUNTIME_EXCEPTION_LATCH = new CountDownLatch(1); + + @OnBinaryMessage + public Uni onMessage(Multi dto) { + return dto.toUni(); + } + + @OnError + public void onError(RuntimeException e) { + RUNTIME_EXCEPTION_LATCH.countDown(); + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/GlobalErrorHandler.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/GlobalErrorHandler.java new file mode 100644 index 0000000000000..424b1b9d34b04 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/GlobalErrorHandler.java @@ -0,0 +1,22 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onerror; + +import java.util.concurrent.CountDownLatch; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.arc.Unremovable; +import io.quarkus.websockets.next.OnError; + +@Unremovable +@ApplicationScoped +public class GlobalErrorHandler { + + public static final CountDownLatch ILLEGAL_ARGUMENT_EXCEPTION_LATCH = new CountDownLatch(1); + + @OnError + public String onError(IllegalArgumentException e) { + ILLEGAL_ARGUMENT_EXCEPTION_LATCH.countDown(); + return e.getMessage(); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/HttpUpgradeFailedEndpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/HttpUpgradeFailedEndpoint.java new file mode 100644 index 0000000000000..7e0d3b6ea0cf5 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/onerror/HttpUpgradeFailedEndpoint.java @@ -0,0 +1,16 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.onerror; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +@WebSocket(path = "/http-upgrade-failed") +public class HttpUpgradeFailedEndpoint { + + @OnTextMessage + public Uni onMessage(Multi dto) { + return dto.toUni(); + } + +} 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 0000000000000..aba3c46ab0d79 --- /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 0000000000000..6f5527583dc1a --- /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/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/BroadcastingEndpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/BroadcastingEndpoint.java new file mode 100644 index 0000000000000..b441bbaa66455 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/BroadcastingEndpoint.java @@ -0,0 +1,14 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/broadcast") +public class BroadcastingEndpoint { + + @OnTextMessage(broadcast = true) + public String onMessage(String message) { + return "echo 0: " + message; + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/ClientEndpointWithPathParams.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/ClientEndpointWithPathParams.java new file mode 100644 index 0000000000000..8957a62ee9149 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/ClientEndpointWithPathParams.java @@ -0,0 +1,13 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocketClient; + +@WebSocketClient(path = "/client-endpoint-with-path-param/{name}") +public class ClientEndpointWithPathParams { + + @OnTextMessage + public void onTextMessage(String message) { + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/Dto.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/Dto.java new file mode 100644 index 0000000000000..585bcb0c950b1 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/Dto.java @@ -0,0 +1,5 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +public record Dto(String property) { + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/DtoTextCodec.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/DtoTextCodec.java new file mode 100644 index 0000000000000..de981e77efccb --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/DtoTextCodec.java @@ -0,0 +1,27 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import java.lang.reflect.Type; + +import jakarta.annotation.Priority; +import jakarta.inject.Singleton; + +import io.quarkus.websockets.next.TextMessageCodec; + +@Priority(15) // this must have higher priority than JsonCodec or tests will be flaky +@Singleton +public class DtoTextCodec implements TextMessageCodec { + @Override + public boolean supports(Type type) { + return type.equals(Dto.class); + } + + @Override + public String encode(Dto dto) { + return dto.property(); + } + + @Override + public Dto decode(Type type, String value) { + return new Dto(value); + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiClient.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiClient.java new file mode 100644 index 0000000000000..edd460601d089 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiClient.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocketClient; +import io.smallrye.mutiny.Multi; + +@WebSocketClient(path = "/multi") +public class MultiClient { + + @OnTextMessage + Multi echo(Multi messages) { + return messages.map(msg -> "echo 0: " + msg); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiDtoReceived_MultiDtoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiDtoReceived_MultiDtoResponse_Endpoint.java new file mode 100644 index 0000000000000..e4c8d3076fe21 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiDtoReceived_MultiDtoResponse_Endpoint.java @@ -0,0 +1,18 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-multi-dto-response-multi-dto") +public class MultiDtoReceived_MultiDtoResponse_Endpoint { + + @OnTextMessage + public Multi onMessage(Multi messages) { + return messages + .map(Dto::property) + .flatMap(msg -> Multi.createFrom().items("echo 0: " + msg, "echo 1: " + msg)) + .map(Dto::new); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiDtoReceived_NoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiDtoReceived_NoResponse_Endpoint.java new file mode 100644 index 0000000000000..9072755e03cf4 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiDtoReceived_NoResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-multi-dto-response-none") +public class MultiDtoReceived_NoResponse_Endpoint { + + @OnTextMessage + public void onMessage(Multi dto) { + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiDtoReceived_SingleDtoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiDtoReceived_SingleDtoResponse_Endpoint.java new file mode 100644 index 0000000000000..a035dd916e7c4 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiDtoReceived_SingleDtoResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-multi-dto-response-single-dto") +public class MultiDtoReceived_SingleDtoResponse_Endpoint { + + @OnTextMessage + public String onMessage(Multi message) { + return "ut labore et dolore magna aliqua"; + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiEndpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiEndpoint.java new file mode 100644 index 0000000000000..e946135b15059 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiEndpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/multi") +public class MultiEndpoint { + + @OnTextMessage + Multi echo(Multi messages) { + return messages.filter(msg -> !msg.startsWith("echo 0: ")); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiTextReceived_MultiTextResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiTextReceived_MultiTextResponse_Endpoint.java new file mode 100644 index 0000000000000..2a267003fa40d --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiTextReceived_MultiTextResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-multi-text-response-multi-text") +public class MultiTextReceived_MultiTextResponse_Endpoint { + + @OnTextMessage + public Multi onMessage(Multi messages) { + return messages.flatMap(msg -> Multi.createFrom().items("echo 0: " + msg, "echo 1: " + msg)); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiTextReceived_NoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiTextReceived_NoResponse_Endpoint.java new file mode 100644 index 0000000000000..c7b5e3e8b14b2 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiTextReceived_NoResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-multi-text-response-none") +public class MultiTextReceived_NoResponse_Endpoint { + + @OnTextMessage + public void onMessage(Multi message) { + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiTextReceived_NoResponse_Endpoint_2.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiTextReceived_NoResponse_Endpoint_2.java new file mode 100644 index 0000000000000..ce9674c33c6b7 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiTextReceived_NoResponse_Endpoint_2.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-multi-text-response-none-2") +public class MultiTextReceived_NoResponse_Endpoint_2 { + + @OnTextMessage + public void onMessage(Multi message) { + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiTextReceived_SingleTextResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiTextReceived_SingleTextResponse_Endpoint.java new file mode 100644 index 0000000000000..7a8f93d65a849 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/MultiTextReceived_SingleTextResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-multi-text-response-single-text") +public class MultiTextReceived_SingleTextResponse_Endpoint { + + @OnTextMessage + public String onMessage(Multi message) { + return "Alpha Shallows"; + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/PingEndpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/PingEndpoint.java new file mode 100644 index 0000000000000..d09ab082348fc --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/PingEndpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.PathParam; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/ping/{one}/and/{two}") +public class PingEndpoint { + + @OnOpen + String process(@PathParam String one, @PathParam String two) { + return one + ":" + two; + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/ServerEndpointWithPathParams.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/ServerEndpointWithPathParams.java new file mode 100644 index 0000000000000..2250581cb68c7 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/ServerEndpointWithPathParams.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Uni; + +@WebSocket(path = "/client-endpoint-with-path-param/{name}") +public class ServerEndpointWithPathParams { + + @OnTextMessage + public Uni onMessage(String message) { + return Uni.createFrom().item("echo 0: " + message); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleDtoReceived_MultiDtoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleDtoReceived_MultiDtoResponse_Endpoint.java new file mode 100644 index 0000000000000..92beae51948bf --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleDtoReceived_MultiDtoResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-single-dto-response-multi-dto") +public class SingleDtoReceived_MultiDtoResponse_Endpoint { + + @OnTextMessage + public Multi onMessage(Dto dto) { + return Multi.createFrom().items("echo 0: " + dto.property(), "echo 1: " + dto.property()).map(Dto::new); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleDtoReceived_NoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleDtoReceived_NoResponse_Endpoint.java new file mode 100644 index 0000000000000..c7d02acd71699 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleDtoReceived_NoResponse_Endpoint.java @@ -0,0 +1,13 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/received-single-dto-response-none") +public class SingleDtoReceived_NoResponse_Endpoint { + + @OnTextMessage + public void onMessage(Dto dto) { + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleDtoReceived_SingleDtoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleDtoReceived_SingleDtoResponse_Endpoint.java new file mode 100644 index 0000000000000..789ad963a95fd --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleDtoReceived_SingleDtoResponse_Endpoint.java @@ -0,0 +1,14 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/received-single-dto-response-single-dto") +public class SingleDtoReceived_SingleDtoResponse_Endpoint { + + @OnTextMessage + public Dto onMessage(Dto dto) { + return new Dto("echo 0: " + dto.property()); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleDtoReceived_UniDtoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleDtoReceived_UniDtoResponse_Endpoint.java new file mode 100644 index 0000000000000..9531792151569 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleDtoReceived_UniDtoResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Uni; + +@WebSocket(path = "/received-single-dto-response-uni-dto") +public class SingleDtoReceived_UniDtoResponse_Endpoint { + + @OnTextMessage + public Uni onMessage(Dto dto) { + return Uni.createFrom().item("echo 0: " + dto.property()).map(Dto::new); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleTextReceived_MultiTextResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleTextReceived_MultiTextResponse_Endpoint.java new file mode 100644 index 0000000000000..55271bf1782fe --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleTextReceived_MultiTextResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/received-single-text-response-multi-text") +public class SingleTextReceived_MultiTextResponse_Endpoint { + + @OnTextMessage + public Multi onMessage(String message) { + return Multi.createFrom().items("echo 0: " + message, "echo 1: " + message); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleTextReceived_NoResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleTextReceived_NoResponse_Endpoint.java new file mode 100644 index 0000000000000..c064f11eaab6e --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleTextReceived_NoResponse_Endpoint.java @@ -0,0 +1,14 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/received-single-text-response-none") +public class SingleTextReceived_NoResponse_Endpoint { + + @OnTextMessage + public void onMessage(String message) { + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleTextReceived_SingleTextSent_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleTextReceived_SingleTextSent_Endpoint.java new file mode 100644 index 0000000000000..04f388c9c4eeb --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleTextReceived_SingleTextSent_Endpoint.java @@ -0,0 +1,14 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/single-text-received-single-text-sent") +public class SingleTextReceived_SingleTextSent_Endpoint { + + @OnTextMessage + public String onMessage(String message) { + return "echo 0: " + message; + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleTextReceived_UniTextResponse_Endpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleTextReceived_UniTextResponse_Endpoint.java new file mode 100644 index 0000000000000..abde015124609 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/telemetry/endpoints/ontextmessage/SingleTextReceived_UniTextResponse_Endpoint.java @@ -0,0 +1,15 @@ +package io.quarkus.websockets.next.test.telemetry.endpoints.ontextmessage; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Uni; + +@WebSocket(path = "/received-single-text-response-uni-text") +public class SingleTextReceived_UniTextResponse_Endpoint { + + @OnTextMessage + public Uni onMessage(String message) { + return Uni.createFrom().item("echo 0: " + message); + } + +} diff --git a/extensions/websockets-next/runtime/pom.xml b/extensions/websockets-next/runtime/pom.xml index 4f0487b590599..d314cd1697715 100644 --- a/extensions/websockets-next/runtime/pom.xml +++ b/extensions/websockets-next/runtime/pom.xml @@ -43,6 +43,18 @@ io.quarkus.security quarkus-security + + + io.opentelemetry + opentelemetry-api + true + + + + io.micrometer + micrometer-core + 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 0000000000000..0cbe298a79435 --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/TelemetryConfig.java @@ -0,0 +1,27 @@ +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(); + + /** + * If collection of WebSocket metrics is enabled. + * Only applicable when the Micrometer extension is present. + */ + @WithName("metrics.enabled") + @WithDefault("true") + boolean metricsEnabled(); + +} 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 90c84b47a90c9..555dfcca35190 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) @@ -63,4 +64,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 650067a60aa41..f6ce209906ed7 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/BasicWebSocketConnectorImpl.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/BasicWebSocketConnectorImpl.java index 6442502058725..c96e0c9143ebc 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/BasicWebSocketConnectorImpl.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/BasicWebSocketConnectorImpl.java @@ -148,7 +148,7 @@ public Uni connect() { codecs, pathParams, serverEndpointUri, - headers, trafficLogger); + headers, trafficLogger, null); if (trafficLogger != null) { trafficLogger.connectionOpened(connection); } 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 12a2b327fa6b1..919efaf0347ae 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 @@ -17,6 +17,8 @@ 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.ErrorInterceptor; +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; @@ -32,7 +34,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) { + Runnable onClose, TelemetrySupport telemetrySupport) { Context context = vertx.getOrCreateContext(); @@ -45,8 +47,8 @@ static void initialize(Vertx vertx, ArcContainer container, Codecs codecs, WebSo container.requestContext()); // Create an endpoint that delegates callbacks to the endpoint bean - WebSocketEndpoint endpoint = createEndpoint(generatedEndpointClass, context, connection, codecs, contextSupport, - securitySupport); + WebSocketEndpoint endpoint = createEndpoint(generatedEndpointClass, connection, codecs, contextSupport, + securitySupport, telemetrySupport); // A broadcast processor is only needed if Multi is consumed by the callback BroadcastProcessor textBroadcastProcessor = endpoint.consumedTextMultiType() != null @@ -357,8 +359,9 @@ public void handle(Void event) { }); } - private static WebSocketEndpoint createEndpoint(String endpointClassName, Context context, - WebSocketConnectionBase connection, Codecs codecs, ContextSupport contextSupport, SecuritySupport securitySupport) { + private static WebSocketEndpoint createEndpoint(String endpointClassName, + WebSocketConnectionBase connection, Codecs codecs, ContextSupport contextSupport, SecuritySupport securitySupport, + TelemetrySupport telemetrySupport) { try { ClassLoader cl = Thread.currentThread().getContextClassLoader(); if (cl == null) { @@ -369,9 +372,10 @@ private static WebSocketEndpoint createEndpoint(String endpointClassName, Contex .loadClass(endpointClassName); WebSocketEndpoint endpoint = (WebSocketEndpoint) endpointClazz .getDeclaredConstructor(WebSocketConnectionBase.class, Codecs.class, ContextSupport.class, - SecuritySupport.class) - .newInstance(connection, codecs, contextSupport, securitySupport); - return endpoint; + SecuritySupport.class, ErrorInterceptor.class) + .newInstance(connection, codecs, contextSupport, securitySupport, + telemetrySupport.getErrorInterceptor()); + return telemetrySupport.decorate(endpoint, connection); } catch (Exception e) { throw new WebSocketException("Unable to create endpoint instance: " + endpointClassName, e); } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketClientConnectionImpl.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketClientConnectionImpl.java index 040f2df87e097..9e20036a17275 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketClientConnectionImpl.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketClientConnectionImpl.java @@ -9,6 +9,7 @@ import io.quarkus.websockets.next.HandshakeRequest; import io.quarkus.websockets.next.WebSocketClientConnection; +import io.quarkus.websockets.next.runtime.telemetry.SendingInterceptor; import io.vertx.core.http.WebSocket; import io.vertx.core.http.WebSocketBase; @@ -20,8 +21,9 @@ class WebSocketClientConnectionImpl extends WebSocketConnectionBase implements W WebSocketClientConnectionImpl(String clientId, WebSocket webSocket, Codecs codecs, Map pathParams, URI serverEndpointUri, Map> headers, - TrafficLogger trafficLogger) { - super(Map.copyOf(pathParams), codecs, new ClientHandshakeRequestImpl(serverEndpointUri, headers), trafficLogger); + TrafficLogger trafficLogger, SendingInterceptor sendingInterceptor) { + super(Map.copyOf(pathParams), codecs, new ClientHandshakeRequestImpl(serverEndpointUri, headers), trafficLogger, + sendingInterceptor); this.clientId = clientId; this.webSocket = Objects.requireNonNull(webSocket); } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java index 4febc7792d813..f68bef1bb9d0f 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java @@ -10,6 +10,7 @@ import io.quarkus.websockets.next.CloseReason; import io.quarkus.websockets.next.HandshakeRequest; import io.quarkus.websockets.next.WebSocketConnection.BroadcastSender; +import io.quarkus.websockets.next.runtime.telemetry.SendingInterceptor; import io.smallrye.mutiny.Uni; import io.vertx.core.buffer.Buffer; import io.vertx.core.buffer.impl.BufferImpl; @@ -21,6 +22,8 @@ public abstract class WebSocketConnectionBase { private static final Logger LOG = Logger.getLogger(WebSocketConnectionBase.class); + private final SendingInterceptor sendingInterceptor; + protected final String identifier; protected final Map pathParams; @@ -34,13 +37,14 @@ public abstract class WebSocketConnectionBase { protected final TrafficLogger trafficLogger; WebSocketConnectionBase(Map pathParams, Codecs codecs, HandshakeRequest handshakeRequest, - TrafficLogger trafficLogger) { + TrafficLogger trafficLogger, SendingInterceptor sendingInterceptor) { this.identifier = UUID.randomUUID().toString(); this.pathParams = pathParams; this.codecs = codecs; this.handshakeRequest = handshakeRequest; this.creationTime = Instant.now(); this.trafficLogger = trafficLogger; + this.sendingInterceptor = sendingInterceptor; } abstract WebSocketBase webSocket(); @@ -54,7 +58,11 @@ public String pathParam(String name) { } public Uni sendText(String message) { - Uni uni = Uni.createFrom().completionStage(() -> webSocket().writeTextMessage(message).toCompletionStage()); + Uni uni = Uni.createFrom() + .completionStage(() -> webSocket().writeTextMessage(message).toCompletionStage()); + if (sendingInterceptor != null) { + uni = uni.invoke(sendingInterceptor.onSend(message)); + } return trafficLogger == null ? uni : uni.invoke(() -> { trafficLogger.textMessageSent(this, message); }); @@ -62,6 +70,9 @@ public Uni sendText(String message) { public Uni sendBinary(Buffer message) { Uni uni = Uni.createFrom().completionStage(() -> webSocket().writeBinaryMessage(message).toCompletionStage()); + if (sendingInterceptor != null) { + uni = uni.invoke(sendingInterceptor.onSend(message)); + } return trafficLogger == null ? uni : uni.invoke(() -> trafficLogger.binaryMessageSent(this, message)); } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java index de23dd4779d78..224709aacd3c7 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java @@ -13,6 +13,7 @@ import io.quarkus.websockets.next.HandshakeRequest; import io.quarkus.websockets.next.WebSocketConnection; +import io.quarkus.websockets.next.runtime.telemetry.SendingInterceptor; import io.smallrye.mutiny.Uni; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.ServerWebSocket; @@ -32,9 +33,10 @@ class WebSocketConnectionImpl extends WebSocketConnectionBase implements WebSock private final BroadcastSender defaultBroadcast; WebSocketConnectionImpl(String generatedEndpointClass, String endpointClass, ServerWebSocket webSocket, - ConnectionManager connectionManager, - Codecs codecs, RoutingContext ctx, TrafficLogger trafficLogger) { - super(Map.copyOf(ctx.pathParams()), codecs, new HandshakeRequestImpl(webSocket, ctx), trafficLogger); + ConnectionManager connectionManager, Codecs codecs, RoutingContext ctx, + TrafficLogger trafficLogger, SendingInterceptor sendingInterceptor) { + super(Map.copyOf(ctx.pathParams()), codecs, new HandshakeRequestImpl(webSocket, ctx), trafficLogger, + sendingInterceptor); this.generatedEndpointClass = generatedEndpointClass; this.endpointId = endpointClass; this.webSocket = Objects.requireNonNull(webSocket); diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java index 686f132c71038..9144f92a3cb51 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java @@ -22,7 +22,9 @@ import io.quarkus.websockets.next.WebSocketsClientRuntimeConfig; import io.quarkus.websockets.next.runtime.WebSocketClientRecorder.ClientEndpoint; import io.quarkus.websockets.next.runtime.WebSocketClientRecorder.ClientEndpointsContext; +import io.quarkus.websockets.next.runtime.telemetry.TelemetrySupportProvider; import io.smallrye.mutiny.Uni; +import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.http.WebSocketClient; import io.vertx.core.http.WebSocketConnectOptions; @@ -35,13 +37,15 @@ public class WebSocketConnectorImpl extends WebSocketConnectorBase connect() { } subprotocols.forEach(connectOptions::addSubProtocol); - return Uni.createFrom().completionStage(() -> client.connect(connectOptions).toCompletionStage()) + var telemetrySupport = telemetrySupportProvider.createClientTelemetrySupport(clientEndpoint.path); + return Uni.createFrom().completionStage(() -> { + if (telemetrySupport.interceptConnection()) { + telemetrySupport.connectionOpened(); + return client.connect(connectOptions).onFailure(new Handler() { + @Override + public void handle(Throwable throwable) { + telemetrySupport.connectionOpeningFailed(throwable); + } + }).toCompletionStage(); + } + return client.connect(connectOptions).toCompletionStage(); + }) .map(ws -> { TrafficLogger trafficLogger = TrafficLogger.forClient(config); WebSocketClientConnectionImpl connection = new WebSocketClientConnectionImpl(clientEndpoint.clientId, ws, codecs, pathParams, - serverEndpointUri, headers, trafficLogger); + serverEndpointUri, headers, trafficLogger, telemetrySupport.getSendingInterceptor()); if (trafficLogger != null) { trafficLogger.connectionOpened(connection); } @@ -106,7 +122,7 @@ public Uni connect() { () -> { connectionManager.remove(clientEndpoint.generatedEndpointClass, connection); client.close(); - }); + }, telemetrySupport); return connection; }); diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java index 937532cd52636..301336839c4cd 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java @@ -16,6 +16,7 @@ import io.quarkus.virtual.threads.VirtualThreadsRecorder; import io.quarkus.websockets.next.InboundProcessingMode; import io.quarkus.websockets.next.runtime.ConcurrencyLimiter.PromiseComplete; +import io.quarkus.websockets.next.runtime.telemetry.ErrorInterceptor; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; import io.vertx.core.Context; @@ -34,6 +35,8 @@ public abstract class WebSocketEndpointBase implements WebSocketEndpoint { protected final Codecs codecs; + private final ErrorInterceptor errorInterceptor; + private final ConcurrencyLimiter limiter; private final ArcContainer container; @@ -47,13 +50,14 @@ public abstract class WebSocketEndpointBase implements WebSocketEndpoint { private final Object beanInstance; public WebSocketEndpointBase(WebSocketConnectionBase connection, Codecs codecs, ContextSupport contextSupport, - SecuritySupport securitySupport) { + SecuritySupport securitySupport, ErrorInterceptor errorInterceptor) { this.connection = connection; this.codecs = codecs; this.limiter = inboundProcessingMode() == InboundProcessingMode.SERIAL ? new ConcurrencyLimiter(connection) : null; this.container = Arc.container(); this.contextSupport = contextSupport; this.securitySupport = securitySupport; + this.errorInterceptor = errorInterceptor; InjectableBean bean = container.bean(beanIdentifier()); if (bean.getScope().equals(ApplicationScoped.class) || bean.getScope().equals(Singleton.class)) { @@ -186,7 +190,7 @@ public Void call() { public Uni doErrorExecute(Throwable throwable, ExecutionModel executionModel, Function> action) { Promise promise = Promise.promise(); - // Always exeute error handler on a new duplicated context + // Always execute error handler on a new duplicated context ContextSupport.createNewDuplicatedContext(Vertx.currentContext(), connection).runOnContext(new Handler() { @Override public void handle(Void event) { @@ -288,10 +292,18 @@ protected Uni doOnClose(Object message) { @Override public Uni doOnError(Throwable t) { - // This method is overriden if there is at least one error handler defined + // This method is overridden if there is at least one error handler defined + interceptError(t); return Uni.createFrom().failure(t); } + // method is used in generated subclasses, if you change a name, change bytecode generation as well + public void interceptError(Throwable t) { + if (errorInterceptor != null) { + errorInterceptor.intercept(t); + } + } + public Object decodeText(Type type, String value, Class codecBeanClass) { return codecs.textDecode(type, value, codecBeanClass); } @@ -345,4 +357,5 @@ public Uni multiBinary(Multi multi, Function LOG.errorf(t, "Unable to send binary message from Multi: %s ", connection)); return Uni.createFrom().voidItem(); } + } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java index 6deedde0f6409..556e5916cd712 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java @@ -21,6 +21,7 @@ import io.quarkus.websockets.next.HttpUpgradeCheck.HttpUpgradeContext; import io.quarkus.websockets.next.WebSocketServerException; import io.quarkus.websockets.next.WebSocketsServerRuntimeConfig; +import io.quarkus.websockets.next.runtime.telemetry.TelemetrySupportProvider; import io.smallrye.common.vertx.VertxContext; import io.smallrye.mutiny.Uni; import io.vertx.core.Context; @@ -85,12 +86,14 @@ public void handle(RoutingContext ctx) { }; } - public Handler createEndpointHandler(String generatedEndpointClass, String endpointId) { + public Handler createEndpointHandler(String generatedEndpointClass, String endpointId, + String endpointPath) { ArcContainer container = Arc.container(); ConnectionManager connectionManager = container.instance(ConnectionManager.class).get(); Codecs codecs = container.instance(Codecs.class).get(); HttpUpgradeCheck[] httpUpgradeChecks = getHttpUpgradeChecks(endpointId, container); TrafficLogger trafficLogger = TrafficLogger.forServer(config); + TelemetrySupportProvider telemetrySupportProvider = container.instance(TelemetrySupportProvider.class).get(); return new Handler() { @Override @@ -113,12 +116,24 @@ public void handle(RoutingContext ctx) { } private void httpUpgrade(RoutingContext ctx) { - Future future = ctx.request().toWebSocket(); + var telemetrySupport = telemetrySupportProvider.createServerTelemetrySupport(endpointPath); + final Future future; + if (telemetrySupport.interceptConnection()) { + telemetrySupport.connectionOpened(); + future = ctx.request().toWebSocket().onFailure(new Handler() { + @Override + public void handle(Throwable throwable) { + telemetrySupport.connectionOpeningFailed(throwable); + } + }); + } else { + future = ctx.request().toWebSocket(); + } future.onSuccess(ws -> { Vertx vertx = VertxCoreRecorder.getVertx().get(); WebSocketConnectionImpl connection = new WebSocketConnectionImpl(generatedEndpointClass, endpointId, ws, - connectionManager, codecs, ctx, trafficLogger); + connectionManager, codecs, ctx, trafficLogger, telemetrySupport.getSendingInterceptor()); connectionManager.add(generatedEndpointClass, connection); if (trafficLogger != null) { trafficLogger.connectionOpened(connection); @@ -128,7 +143,7 @@ private void httpUpgrade(RoutingContext ctx) { Endpoints.initialize(vertx, container, codecs, connection, ws, generatedEndpointClass, config.autoPingInterval(), securitySupport, config.unhandledFailureStrategy(), trafficLogger, - () -> connectionManager.remove(generatedEndpointClass, connection)); + () -> connectionManager.remove(generatedEndpointClass, connection), telemetrySupport); }); } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/AbstractWebSocketEndpointWrapper.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/AbstractWebSocketEndpointWrapper.java new file mode 100644 index 0000000000000..cf5b4a592d7ab --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/AbstractWebSocketEndpointWrapper.java @@ -0,0 +1,107 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +import java.lang.reflect.Type; + +import io.quarkus.websockets.next.InboundProcessingMode; +import io.quarkus.websockets.next.runtime.WebSocketEndpoint; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Future; +import io.vertx.core.buffer.Buffer; + +/** + * {@link WebSocketEndpoint} wrapper that delegates all methods to {@link #delegate}. + * This way, subclasses can only override methods they need to intercept. + */ +abstract class AbstractWebSocketEndpointWrapper implements WebSocketEndpoint { + + protected final WebSocketEndpoint delegate; + + protected AbstractWebSocketEndpointWrapper(WebSocketEndpoint delegate) { + this.delegate = delegate; + } + + @Override + public InboundProcessingMode inboundProcessingMode() { + return delegate.inboundProcessingMode(); + } + + @Override + public Future onOpen() { + return delegate.onOpen(); + } + + @Override + public ExecutionModel onOpenExecutionModel() { + return delegate.onOpenExecutionModel(); + } + + @Override + public Future onTextMessage(Object message) { + return delegate.onTextMessage(message); + } + + @Override + public ExecutionModel onTextMessageExecutionModel() { + return delegate.onTextMessageExecutionModel(); + } + + @Override + public Type consumedTextMultiType() { + return delegate.consumedTextMultiType(); + } + + @Override + public Object decodeTextMultiItem(Object message) { + return delegate.decodeTextMultiItem(message); + } + + @Override + public Future onBinaryMessage(Object message) { + return delegate.onBinaryMessage(message); + } + + @Override + public ExecutionModel onBinaryMessageExecutionModel() { + return delegate.onBinaryMessageExecutionModel(); + } + + @Override + public Type consumedBinaryMultiType() { + return delegate.consumedBinaryMultiType(); + } + + @Override + public Object decodeBinaryMultiItem(Object message) { + return delegate.decodeBinaryMultiItem(message); + } + + @Override + public Future onPongMessage(Buffer message) { + return delegate.onPongMessage(message); + } + + @Override + public ExecutionModel onPongMessageExecutionModel() { + return delegate.onPongMessageExecutionModel(); + } + + @Override + public Future onClose() { + return delegate.onClose(); + } + + @Override + public ExecutionModel onCloseExecutionModel() { + return delegate.onCloseExecutionModel(); + } + + @Override + public Uni doOnError(Throwable t) { + return delegate.doOnError(t); + } + + @Override + public String beanIdentifier() { + return delegate.beanIdentifier(); + } +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/ConnectionInterceptor.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/ConnectionInterceptor.java new file mode 100644 index 0000000000000..6207f7ad39e61 --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/ConnectionInterceptor.java @@ -0,0 +1,53 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public sealed interface ConnectionInterceptor permits MetricsConnectionInterceptor, TracesConnectionInterceptor, + ConnectionInterceptor.CompositeConnectionInterceptor { + + void connectionOpened(); + + void connectionOpeningFailed(Throwable cause); + + /** + * Way to pass a context between {@link ConnectionInterceptor} and telemetry endpoint decorators. + * + * @return unmodifiable map passed to decorators as {@link TelemetryWebSocketEndpointContext#contextData()} + */ + Map getContextData(); + + final class CompositeConnectionInterceptor implements ConnectionInterceptor { + + private final List leaves; + + CompositeConnectionInterceptor(List leaves) { + this.leaves = List.copyOf(leaves); + } + + @Override + public void connectionOpened() { + for (var leaf : leaves) { + leaf.connectionOpened(); + } + } + + @Override + public void connectionOpeningFailed(Throwable cause) { + for (var leaf : leaves) { + leaf.connectionOpeningFailed(cause); + } + } + + @Override + public Map getContextData() { + Map map = new HashMap<>(); + for (var leaf : leaves) { + map.putAll(leaf.getContextData()); + } + return Collections.unmodifiableMap(map); + } + } +} 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 new file mode 100644 index 0000000000000..f210470be07b0 --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/ErrorCountingInterceptor.java @@ -0,0 +1,17 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +import io.micrometer.core.instrument.Counter; + +final class ErrorCountingInterceptor implements ErrorInterceptor { + + private final Counter errorCounter; + + ErrorCountingInterceptor(Counter errorCounter) { + this.errorCounter = errorCounter; + } + + @Override + public void intercept(Throwable throwable) { + errorCounter.increment(); + } +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/ErrorInterceptor.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/ErrorInterceptor.java new file mode 100644 index 0000000000000..b7bcb4badad5b --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/ErrorInterceptor.java @@ -0,0 +1,12 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +/** + * Error interceptor must be used to intercept + * {@link io.quarkus.websockets.next.runtime.WebSocketEndpoint#doOnError(Throwable)}. + * The 'doOnError' method is called from within the class and using an endpoint wrapper wouldn't be sufficient. + */ +public sealed interface ErrorInterceptor permits ErrorCountingInterceptor { + + void intercept(Throwable throwable); + +} 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 new file mode 100644 index 0000000000000..a8f26d66b20e6 --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsBuilderCustomizer.java @@ -0,0 +1,190 @@ +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_MESSAGES_COUNT_ERRORS; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CLIENT_MESSAGES_COUNT_RECEIVED_BYTES; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CLIENT_MESSAGES_COUNT_SENT; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CLIENT_MESSAGES_COUNT_SENT_BYTES; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_CLOSED; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_OPENED; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_OPENED_ERROR; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_MESSAGES_COUNT_ERRORS; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_MESSAGES_COUNT_RECEIVED; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_MESSAGES_COUNT_RECEIVED_BYTES; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_MESSAGES_COUNT_SENT_BYTES; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.URI_ATTR_KEY; + +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 io.quarkus.arc.Arc; +import io.quarkus.websockets.next.WebSocketsClientRuntimeConfig; +import io.quarkus.websockets.next.WebSocketsServerRuntimeConfig; +import io.quarkus.websockets.next.runtime.WebSocketEndpoint; + +/** + * Installs metrics support into the WebSockets extension. + */ +public final class MetricsBuilderCustomizer implements Consumer { + @Override + public void accept(TelemetrySupportProviderBuilder builder) { + var container = Arc.container(); + + var serverMetricsEnabled = container.instance(WebSocketsServerRuntimeConfig.class).get().telemetry().metricsEnabled(); + var clientMetricsEnabled = container.instance(WebSocketsClientRuntimeConfig.class).get().telemetry().metricsEnabled(); + if (clientMetricsEnabled || serverMetricsEnabled) { + var registry = container.instance(MeterRegistry.class).get(); + if (clientMetricsEnabled) { + addClientMetricsSupport(builder, registry); + } + if (serverMetricsEnabled) { + addServerMetricsSupport(builder, registry); + } + } + } + + private static void addServerMetricsSupport(TelemetrySupportProviderBuilder builder, MeterRegistry registry) { + builder.serverEndpointDecorator(new Function<>() { + + private final Meter.MeterProvider receivedMessagesCounter = Counter + .builder(SERVER_MESSAGES_COUNT_RECEIVED) + .description("Number of messages received by server endpoints.") + .withRegistry(registry); + + private final Meter.MeterProvider receivedBytesCounter = Counter + .builder(SERVER_MESSAGES_COUNT_RECEIVED_BYTES) + .description("Number of bytes received by server endpoints.") + .withRegistry(registry); + + private final Meter.MeterProvider closedConnectionCounter = Counter + .builder(SERVER_CONNECTION_CLOSED) + .description("Number of closed server WebSocket connections.") + .withRegistry(registry); + + @Override + public WebSocketEndpoint apply(TelemetryWebSocketEndpointContext ctx) { + return new MetricsWebSocketEndpointWrapper(ctx.endpoint(), + receivedMessagesCounter.withTag(URI_ATTR_KEY, ctx.path()), + receivedBytesCounter.withTag(URI_ATTR_KEY, ctx.path()), + closedConnectionCounter.withTag(URI_ATTR_KEY, ctx.path())); + } + }); + builder.pathToServerErrorInterceptor(new Function<>() { + + private final Meter.MeterProvider serverErrorsCounter = Counter + .builder(SERVER_MESSAGES_COUNT_ERRORS) + .description("Counts all the WebSockets server endpoint errors.") + .withRegistry(registry); + + @Override + public ErrorInterceptor apply(String path) { + return new ErrorCountingInterceptor(serverErrorsCounter.withTag(URI_ATTR_KEY, path)); + } + }); + builder.pathToServerSendingInterceptor(new Function<>() { + + private final Meter.MeterProvider sentBytesCounter = Counter + .builder(SERVER_MESSAGES_COUNT_SENT_BYTES) + .description("Number of bytes sent from server endpoints.") + .withRegistry(registry); + + @Override + public SendingInterceptor apply(String path) { + return new MetricsSendingInterceptor(sentBytesCounter.withTag(URI_ATTR_KEY, path)); + } + }); + builder.pathToServerConnectionInterceptor(new Function<>() { + + private final Meter.MeterProvider connectionOpenCounter = Counter + .builder(SERVER_CONNECTION_OPENED) + .description("Number of opened server connections.") + .withRegistry(registry); + + private final Meter.MeterProvider connectionOpeningFailedCounter = Counter + .builder(SERVER_CONNECTION_OPENED_ERROR) + .description("Number of failures occurred when opening server connection failed.") + .withRegistry(registry); + + @Override + public ConnectionInterceptor apply(String path) { + return new MetricsConnectionInterceptor(connectionOpenCounter.withTag(URI_ATTR_KEY, path), + connectionOpeningFailedCounter.withTag(URI_ATTR_KEY, path)); + } + }); + } + + private static void addClientMetricsSupport(TelemetrySupportProviderBuilder builder, MeterRegistry registry) { + builder.clientEndpointDecorator(new Function<>() { + + private final Meter.MeterProvider receivedBytesCounter = Counter + .builder(CLIENT_MESSAGES_COUNT_RECEIVED_BYTES) + .description("Number of bytes received by client endpoints.") + .withRegistry(registry); + + private final Meter.MeterProvider closedConnectionCounter = Counter + .builder(CLIENT_CONNECTION_CLOSED) + .description("Number of closed client WebSocket connections.") + .withRegistry(registry); + + @Override + public WebSocketEndpoint apply(TelemetryWebSocketEndpointContext ctx) { + return new MetricsWebSocketEndpointWrapper(ctx.endpoint(), + receivedBytesCounter.withTag(URI_ATTR_KEY, ctx.path()), + closedConnectionCounter.withTag(URI_ATTR_KEY, ctx.path())); + } + }); + builder.pathToClientErrorInterceptor(new Function<>() { + + private final Meter.MeterProvider clientErrorsCounter = Counter + .builder(CLIENT_MESSAGES_COUNT_ERRORS) + .description("Counts all the WebSockets client endpoint errors.") + .withRegistry(registry); + + @Override + public ErrorInterceptor apply(String path) { + return new ErrorCountingInterceptor(clientErrorsCounter.withTag(URI_ATTR_KEY, path)); + } + }); + builder.pathToClientSendingInterceptor(new Function<>() { + + private final Meter.MeterProvider sentBytesCounter = Counter + .builder(CLIENT_MESSAGES_COUNT_SENT_BYTES) + .description("Number of bytes sent from client endpoints.") + .withRegistry(registry); + + private final Meter.MeterProvider sentMessagesCounter = Counter + .builder(CLIENT_MESSAGES_COUNT_SENT) + .description("Number of messages sent from client endpoints.") + .withRegistry(registry); + + @Override + public SendingInterceptor apply(String path) { + return new MetricsSendingInterceptor(sentMessagesCounter.withTag(URI_ATTR_KEY, path), + sentBytesCounter.withTag(URI_ATTR_KEY, path)); + } + }); + builder.pathToClientConnectionInterceptor(new Function<>() { + + private final Meter.MeterProvider connectionOpenCounter = Counter + .builder(CLIENT_CONNECTION_OPENED) + .description("Number of opened client connections.") + .withRegistry(registry); + + private final Meter.MeterProvider connectionOpeningFailedCounter = Counter + .builder(CLIENT_CONNECTION_OPENED_ERROR) + .description("Number of failures occurred when opening client connection failed.") + .withRegistry(registry); + + @Override + public ConnectionInterceptor apply(String path) { + return new MetricsConnectionInterceptor(connectionOpenCounter.withTag(URI_ATTR_KEY, path), + connectionOpeningFailedCounter.withTag(URI_ATTR_KEY, path)); + } + }); + } +} 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 new file mode 100644 index 0000000000000..015878dc0a670 --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsConnectionInterceptor.java @@ -0,0 +1,31 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +import java.util.Map; + +import io.micrometer.core.instrument.Counter; + +final class MetricsConnectionInterceptor implements ConnectionInterceptor { + + private final Counter connectionOpenCounter; + private final Counter connectionOpeninigFailedCounter; + + MetricsConnectionInterceptor(Counter connectionOpenCounter, Counter connectionOpeninigFailedCounter) { + this.connectionOpenCounter = connectionOpenCounter; + this.connectionOpeninigFailedCounter = connectionOpeninigFailedCounter; + } + + @Override + public void connectionOpened() { + connectionOpenCounter.increment(); + } + + @Override + public void connectionOpeningFailed(Throwable cause) { + connectionOpeninigFailedCounter.increment(); + } + + @Override + public Map getContextData() { + return Map.of(); + } +} 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 new file mode 100644 index 0000000000000..10043b02f8d5d --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsSendingInterceptor.java @@ -0,0 +1,48 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +import java.nio.charset.StandardCharsets; + +import io.micrometer.core.instrument.Counter; +import io.vertx.core.buffer.Buffer; + +final class MetricsSendingInterceptor implements SendingInterceptor { + + private final Counter onMessageSentCounter; + private final Counter onMessageSentBytesCounter; + + MetricsSendingInterceptor(Counter onMessageSentBytesCounter) { + this.onMessageSentCounter = null; + this.onMessageSentBytesCounter = onMessageSentBytesCounter; + } + + MetricsSendingInterceptor(Counter onMessageSentCounter, Counter onMessageSentBytesCounter) { + this.onMessageSentCounter = onMessageSentCounter; + this.onMessageSentBytesCounter = onMessageSentBytesCounter; + } + + @Override + public Runnable onSend(String text) { + return new Runnable() { + @Override + public void run() { + if (onMessageSentCounter != null) { + onMessageSentCounter.increment(); + } + onMessageSentBytesCounter.increment(text.getBytes(StandardCharsets.UTF_8).length); + } + }; + } + + @Override + public Runnable onSend(Buffer message) { + return new Runnable() { + @Override + public void run() { + if (onMessageSentCounter != null) { + onMessageSentCounter.increment(); + } + onMessageSentBytesCounter.increment(message.getBytes().length); + } + }; + } +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsWebSocketEndpointWrapper.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsWebSocketEndpointWrapper.java new file mode 100644 index 0000000000000..ae148a947b69b --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/MetricsWebSocketEndpointWrapper.java @@ -0,0 +1,79 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +import io.micrometer.core.instrument.Counter; +import io.quarkus.websockets.next.runtime.WebSocketEndpoint; +import io.vertx.core.Future; +import io.vertx.core.buffer.Buffer; + +final class MetricsWebSocketEndpointWrapper extends AbstractWebSocketEndpointWrapper { + + private final Counter onMessageReceivedCounter; + private final Counter onMessageReceivedBytesCounter; + private final Counter onConnectionClosedCounter; + + MetricsWebSocketEndpointWrapper(WebSocketEndpoint delegate, Counter onMessageReceivedBytesCounter, + Counter onConnectionClosedCounter) { + super(delegate); + this.onMessageReceivedCounter = null; + this.onMessageReceivedBytesCounter = onMessageReceivedBytesCounter; + this.onConnectionClosedCounter = onConnectionClosedCounter; + } + + MetricsWebSocketEndpointWrapper(WebSocketEndpoint delegate, Counter onMessageReceivedCounter, + Counter onMessageReceivedBytesCounter, Counter onConnectionClosedCounter) { + super(delegate); + this.onMessageReceivedCounter = onMessageReceivedCounter; + this.onMessageReceivedBytesCounter = onMessageReceivedBytesCounter; + this.onConnectionClosedCounter = onConnectionClosedCounter; + } + + @Override + public Future onTextMessage(Object message) { + addMetricsIfMessageIsString(message); + return delegate.onTextMessage(message); + } + + @Override + public Future onBinaryMessage(Object message) { + addMetricsIfMessageIsBuffer(message); + return delegate.onBinaryMessage(message); + } + + @Override + public Object decodeTextMultiItem(Object message) { + addMetricsIfMessageIsString(message); + return delegate.decodeTextMultiItem(message); + } + + @Override + public Object decodeBinaryMultiItem(Object message) { + addMetricsIfMessageIsBuffer(message); + return delegate.decodeBinaryMultiItem(message); + } + + @Override + public Future onClose() { + onConnectionClosedCounter.increment(); + return delegate.onClose(); + } + + private void addMetricsIfMessageIsString(Object message) { + if (message instanceof String stringMessage) { + if (onMessageReceivedCounter != null) { + onMessageReceivedCounter.increment(); + } + double bytesNum = stringMessage.getBytes().length; + onMessageReceivedBytesCounter.increment(bytesNum); + } + } + + private void addMetricsIfMessageIsBuffer(Object message) { + if (message instanceof Buffer bufferMessage) { + if (onMessageReceivedCounter != null) { + onMessageReceivedCounter.increment(); + } + double bytesNum = bufferMessage.getBytes().length; + onMessageReceivedBytesCounter.increment(bytesNum); + } + } +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/SendingInterceptor.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/SendingInterceptor.java new file mode 100644 index 0000000000000..03cca02102c43 --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/SendingInterceptor.java @@ -0,0 +1,16 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +import io.vertx.core.buffer.Buffer; + +/** + * Intercepts text or binary send from the {@link io.quarkus.websockets.next.runtime.WebSocketConnectionBase} connection. + */ +public sealed interface SendingInterceptor permits MetricsSendingInterceptor { + + // following methods should mirror WebSocketConnectionBase sending methods + + Runnable onSend(String text); + + Runnable onSend(Buffer message); + +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryConstants.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryConstants.java new file mode 100644 index 0000000000000..fdaf2a2ade15c --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryConstants.java @@ -0,0 +1,99 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +public interface TelemetryConstants { + + /** + * OpenTelemetry name for Spans created for opened client WebSocket connections. + * Metric key for opened server WebSocket connections. + */ + String CLIENT_CONNECTION_OPENED = "quarkus.websockets.client.connection.opened"; + + /** + * OpenTelemetry name for Spans created for opened server WebSocket connections. + * Metric key for opened server WebSocket connections. + */ + String SERVER_CONNECTION_OPENED = "quarkus.websockets.server.connection.opened"; + + /** + * OpenTelemetry name for Spans created when opening of a client WebSocket connection fails. + */ + String CLIENT_CONNECTION_OPENED_ERROR = "quarkus.websockets.client.connection.opened.error"; + + /** + * OpenTelemetry name for Spans created when opening of a server WebSocket connection fails. + */ + String SERVER_CONNECTION_OPENED_ERROR = "quarkus.websockets.server.connection.opened.error"; + + /** + * OpenTelemetry name for Spans created for closed client WebSocket connections. + * Metric key for closed client WebSocket connections. + */ + String CLIENT_CONNECTION_CLOSED = "quarkus.websockets.client.connection.closed"; + + /** + * OpenTelemetry name for Spans created for closed server WebSocket connections. + * Metric key for closed server WebSocket connections. + */ + String SERVER_CONNECTION_CLOSED = "quarkus.websockets.server.connection.closed"; + + /** + * OpenTelemetry attribute added to {@link #SERVER_CONNECTION_OPENED_ERROR} or {@link #CLIENT_CONNECTION_OPENED_ERROR}. + * It contains error messages of the failure that prevent a WebSocket connection from opening. + */ + String CONNECTION_FAILURE_ATTR_KEY = "connection.failure"; + + /** + * OpenTelemetry attributes added to {@link #CLIENT_CONNECTION_CLOSED} or {@link #SERVER_CONNECTION_CLOSED} spans. + */ + String CONNECTION_ID_ATTR_KEY = "connection.id"; + String CONNECTION_ENDPOINT_ATTR_KEY = "connection.endpoint.id"; + String CONNECTION_CLIENT_ATTR_KEY = "connection.client.id"; + + /** + * WebSocket endpoint path (with path params in it). + * This attribute is added to created OpenTelemetry spans. + * Micrometer metrics are tagged with URI as well. + */ + String URI_ATTR_KEY = "uri"; + + /** + * Number of messages received by server endpoints. + */ + String SERVER_MESSAGES_COUNT_RECEIVED = "quarkus.websockets.server.messages.count.received"; + + /** + * Counts all the WebSockets server endpoint errors. + */ + String SERVER_MESSAGES_COUNT_ERRORS = "quarkus.websockets.server.messages.count.errors"; + + /** + * Number of bytes sent from server endpoints. + */ + String SERVER_MESSAGES_COUNT_SENT_BYTES = "quarkus.websockets.server.messages.count.sent.bytes"; + + /** + * Number of bytes received by server endpoints. + */ + String SERVER_MESSAGES_COUNT_RECEIVED_BYTES = "quarkus.websockets.server.messages.count.received.bytes"; + + /** + * Number of messages sent from client endpoints. + */ + String CLIENT_MESSAGES_COUNT_SENT = "quarkus.websockets.client.messages.count.sent"; + + /** + * Counts all the WebSockets client endpoint errors. + */ + String CLIENT_MESSAGES_COUNT_ERRORS = "quarkus.websockets.client.messages.count.errors"; + + /** + * Number of bytes sent from client endpoints. + */ + String CLIENT_MESSAGES_COUNT_SENT_BYTES = "quarkus.websockets.client.messages.count.sent.bytes"; + + /** + * Number of bytes received by client endpoints. + */ + String CLIENT_MESSAGES_COUNT_RECEIVED_BYTES = "quarkus.websockets.client.messages.count.received.bytes"; + +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetrySupport.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetrySupport.java new file mode 100644 index 0000000000000..414c4e8072774 --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetrySupport.java @@ -0,0 +1,71 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +import java.util.Map; + +import io.quarkus.websockets.next.runtime.WebSocketConnectionBase; +import io.quarkus.websockets.next.runtime.WebSocketEndpoint; + +/** + * Integrates metrics and traces into WebSockets with {@link SendingInterceptor}, {@link ErrorInterceptor} + * and {@link WebSocketEndpoint} decorator. + */ +public abstract class TelemetrySupport { + + static final TelemetrySupport EMPTY = new TelemetrySupport(null, null, null) { + + @Override + public WebSocketEndpoint decorate(WebSocketEndpoint endpoint, WebSocketConnectionBase connection) { + return endpoint; + } + + @Override + public boolean interceptConnection() { + return false; + } + }; + + private final SendingInterceptor sendingInterceptor; + private final ErrorInterceptor errorInterceptor; + private final ConnectionInterceptor connectionInterceptor; + + TelemetrySupport(SendingInterceptor sendingInterceptor, ErrorInterceptor errorInterceptor, + ConnectionInterceptor connectionInterceptor) { + this.sendingInterceptor = sendingInterceptor; + this.errorInterceptor = errorInterceptor; + this.connectionInterceptor = connectionInterceptor; + } + + public abstract WebSocketEndpoint decorate(WebSocketEndpoint endpoint, WebSocketConnectionBase connection); + + public SendingInterceptor getSendingInterceptor() { + return sendingInterceptor; + } + + public ErrorInterceptor getErrorInterceptor() { + return errorInterceptor; + } + + public boolean interceptConnection() { + return connectionInterceptor != null; + } + + /** + * Collects telemetry when WebSocket connection is opened. + * Only supported when {@link #interceptConnection()}. + */ + public void connectionOpened() { + connectionInterceptor.connectionOpened(); + } + + /** + * Collects telemetry when WebSocket connection opening failed. + * Only supported when {@link #interceptConnection()}. + */ + public void connectionOpeningFailed(Throwable throwable) { + connectionInterceptor.connectionOpeningFailed(throwable); + } + + protected Map getContextData() { + return connectionInterceptor == null ? Map.of() : connectionInterceptor.getContextData(); + } +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetrySupportProvider.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetrySupportProvider.java new file mode 100644 index 0000000000000..f1be5293475a7 --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetrySupportProvider.java @@ -0,0 +1,120 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +import java.util.function.Function; + +import io.quarkus.websockets.next.runtime.WebSocketConnectionBase; +import io.quarkus.websockets.next.runtime.WebSocketEndpoint; + +public final class TelemetrySupportProvider { + + private final Function pathToClientSendingInterceptor; + private final Function pathToServerSendingInterceptor; + private final Function pathToClientErrorInterceptor; + private final Function pathToServerErrorInterceptor; + private final Function serverEndpointDecorator; + private final Function clientEndpointDecorator; + private final Function pathToClientConnectionInterceptor; + private final Function pathToServerConnectionInterceptor; + private final boolean clientTelemetryEnabled; + private final boolean serverTelemetryEnabled; + + TelemetrySupportProvider(Function pathToClientSendingInterceptor, + Function pathToServerSendingInterceptor, + Function pathToClientErrInterceptor, + Function pathToServerErrInterceptor, + Function serverEndpointDecorator, + Function clientEndpointDecorator, + Function pathToClientConnectionInterceptor, + Function pathToServerConnectionInterceptor) { + this.serverTelemetryEnabled = pathToServerSendingInterceptor != null || pathToServerErrInterceptor != null + || serverEndpointDecorator != null || pathToServerConnectionInterceptor != null; + this.pathToServerSendingInterceptor = pathToServerSendingInterceptor; + this.pathToServerErrorInterceptor = pathToServerErrInterceptor; + this.serverEndpointDecorator = serverEndpointDecorator; + this.pathToServerConnectionInterceptor = pathToServerConnectionInterceptor; + this.clientTelemetryEnabled = clientEndpointDecorator != null || pathToClientSendingInterceptor != null + || pathToClientErrInterceptor != null || pathToClientConnectionInterceptor != null; + this.clientEndpointDecorator = clientEndpointDecorator; + this.pathToClientSendingInterceptor = pathToClientSendingInterceptor; + this.pathToClientErrorInterceptor = pathToClientErrInterceptor; + this.pathToClientConnectionInterceptor = pathToClientConnectionInterceptor; + } + + TelemetrySupportProvider() { + this.pathToClientSendingInterceptor = null; + this.pathToServerSendingInterceptor = null; + this.pathToClientErrorInterceptor = null; + this.pathToServerErrorInterceptor = null; + this.pathToClientConnectionInterceptor = null; + this.pathToServerConnectionInterceptor = null; + this.serverEndpointDecorator = null; + this.clientEndpointDecorator = null; + this.serverTelemetryEnabled = false; + this.clientTelemetryEnabled = false; + } + + /** + * This method may only be called on the Vert.x context of the initial HTTP request as it collects context data. + * + * @param path endpoint path with path param placeholders + * @return TelemetryDecorator + */ + public TelemetrySupport createServerTelemetrySupport(String path) { + if (serverTelemetryEnabled) { + return new TelemetrySupport(getServerSendingInterceptor(path), getServerErrorInterceptor(path), + getServerConnectionInterceptor(path)) { + @Override + public WebSocketEndpoint decorate(WebSocketEndpoint endpoint, WebSocketConnectionBase connection) { + if (serverEndpointDecorator == null) { + return endpoint; + } + return serverEndpointDecorator + .apply(new TelemetryWebSocketEndpointContext(endpoint, connection, path, getContextData())); + } + }; + } + return TelemetrySupport.EMPTY; + } + + public TelemetrySupport createClientTelemetrySupport(String path) { + if (clientTelemetryEnabled) { + return new TelemetrySupport(getClientSendingInterceptor(path), getClientErrorInterceptor(path), + getClientConnectionInterceptor(path)) { + @Override + public WebSocketEndpoint decorate(WebSocketEndpoint endpoint, WebSocketConnectionBase connection) { + if (clientEndpointDecorator == null) { + return endpoint; + } + return clientEndpointDecorator + .apply(new TelemetryWebSocketEndpointContext(endpoint, connection, path, getContextData())); + } + }; + } + return TelemetrySupport.EMPTY; + } + + private ErrorInterceptor getServerErrorInterceptor(String path) { + return pathToServerErrorInterceptor == null ? null : pathToServerErrorInterceptor.apply(path); + } + + private SendingInterceptor getServerSendingInterceptor(String path) { + return pathToServerSendingInterceptor == null ? null : pathToServerSendingInterceptor.apply(path); + } + + private ConnectionInterceptor getServerConnectionInterceptor(String path) { + return pathToServerConnectionInterceptor == null ? null : pathToServerConnectionInterceptor.apply(path); + } + + private ErrorInterceptor getClientErrorInterceptor(String path) { + return pathToClientErrorInterceptor == null ? null : pathToClientErrorInterceptor.apply(path); + } + + private SendingInterceptor getClientSendingInterceptor(String path) { + return pathToClientSendingInterceptor == null ? null : pathToClientSendingInterceptor.apply(path); + } + + private ConnectionInterceptor getClientConnectionInterceptor(String path) { + return pathToClientConnectionInterceptor == null ? null : pathToClientConnectionInterceptor.apply(path); + } + +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetrySupportProviderBuilder.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetrySupportProviderBuilder.java new file mode 100644 index 0000000000000..1d49d53310027 --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetrySupportProviderBuilder.java @@ -0,0 +1,160 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import io.quarkus.websockets.next.runtime.WebSocketEndpoint; +import io.quarkus.websockets.next.runtime.telemetry.ConnectionInterceptor.CompositeConnectionInterceptor; + +/** + * Quarkus uses this class internally to build {@link TelemetrySupportProvider}. + */ +public final class TelemetrySupportProviderBuilder { + + private Function pathToClientSendingInterceptor; + private Function pathToServerSendingInterceptor; + private Function pathToClientErrorInterceptor; + private Function pathToServerErrorInterceptor; + private Function pathToClientConnectionInterceptor; + private Function pathToServerConnectionInterceptor; + private Function serverEndpointDecorator; + private Function clientEndpointDecorator; + private boolean telemetryEnabled; + + TelemetrySupportProviderBuilder() { + pathToClientSendingInterceptor = null; + pathToServerSendingInterceptor = null; + pathToClientErrorInterceptor = null; + pathToServerErrorInterceptor = null; + serverEndpointDecorator = null; + clientEndpointDecorator = null; + telemetryEnabled = false; + } + + void clientEndpointDecorator(Function decorator) { + Objects.requireNonNull(decorator); + if (this.clientEndpointDecorator == null) { + this.telemetryEnabled = true; + this.clientEndpointDecorator = decorator; + } else { + this.clientEndpointDecorator = this.clientEndpointDecorator + .compose(new Function() { + @Override + public TelemetryWebSocketEndpointContext apply(TelemetryWebSocketEndpointContext ctx) { + var decorated = decorator.apply(ctx); + return new TelemetryWebSocketEndpointContext(decorated, ctx.connection(), ctx.path(), + ctx.contextData()); + } + }); + } + } + + void serverEndpointDecorator(Function decorator) { + Objects.requireNonNull(decorator); + if (this.serverEndpointDecorator == null) { + this.telemetryEnabled = true; + this.serverEndpointDecorator = decorator; + } else { + this.serverEndpointDecorator = this.serverEndpointDecorator + .compose(new Function() { + @Override + public TelemetryWebSocketEndpointContext apply(TelemetryWebSocketEndpointContext ctx) { + var decorated = decorator.apply(ctx); + return new TelemetryWebSocketEndpointContext(decorated, ctx.connection(), ctx.path(), + ctx.contextData()); + } + }); + } + } + + void pathToServerErrorInterceptor(Function pathToServerErrorInterceptor) { + Objects.requireNonNull(pathToServerErrorInterceptor); + if (this.pathToServerErrorInterceptor == null) { + this.telemetryEnabled = true; + this.pathToServerErrorInterceptor = pathToServerErrorInterceptor; + } else { + // we can implement composite if we need this in the future + throw new IllegalStateException("Only one server ErrorInterceptor is supported"); + } + } + + void pathToClientErrorInterceptor(Function pathToClientErrorInterceptor) { + Objects.requireNonNull(pathToClientErrorInterceptor); + if (this.pathToClientErrorInterceptor == null) { + this.telemetryEnabled = true; + this.pathToClientErrorInterceptor = pathToClientErrorInterceptor; + } else { + // we can implement composite if we need this in the future + throw new IllegalStateException("Only one client ErrorInterceptor is supported"); + } + } + + void pathToServerSendingInterceptor(Function pathToServerSendingInterceptor) { + Objects.requireNonNull(pathToServerSendingInterceptor); + if (this.pathToServerSendingInterceptor == null) { + this.telemetryEnabled = true; + this.pathToServerSendingInterceptor = pathToServerSendingInterceptor; + } else { + // we can implement composite if we need this in the future + throw new IllegalStateException("Only one server SendingInterceptor is supported"); + } + } + + void pathToClientSendingInterceptor(Function pathToClientSendingInterceptor) { + Objects.requireNonNull(pathToClientSendingInterceptor); + if (this.pathToClientSendingInterceptor == null) { + this.telemetryEnabled = true; + this.pathToClientSendingInterceptor = pathToClientSendingInterceptor; + } else { + // we can implement composite if we need this in the future + throw new IllegalStateException("Only one client SendingInterceptor is supported"); + } + } + + void pathToClientConnectionInterceptor(Function pathToInterceptor1) { + Objects.requireNonNull(pathToInterceptor1); + if (this.pathToClientConnectionInterceptor == null) { + this.telemetryEnabled = true; + this.pathToClientConnectionInterceptor = pathToInterceptor1; + } else { + var pathToInterceptor2 = this.pathToClientConnectionInterceptor; + this.pathToClientConnectionInterceptor = new Function<>() { + @Override + public ConnectionInterceptor apply(String path) { + var interceptor1 = pathToInterceptor1.apply(path); + var interceptor2 = pathToInterceptor2.apply(path); + return new CompositeConnectionInterceptor(List.of(interceptor1, interceptor2)); + } + }; + } + } + + void pathToServerConnectionInterceptor(Function pathToInterceptor1) { + Objects.requireNonNull(pathToInterceptor1); + if (this.pathToServerConnectionInterceptor == null) { + this.telemetryEnabled = true; + this.pathToServerConnectionInterceptor = pathToInterceptor1; + } else { + var pathToInterceptor2 = this.pathToServerConnectionInterceptor; + this.pathToServerConnectionInterceptor = new Function<>() { + @Override + public ConnectionInterceptor apply(String path) { + var interceptor1 = pathToInterceptor1.apply(path); + var interceptor2 = pathToInterceptor2.apply(path); + return new CompositeConnectionInterceptor(List.of(interceptor1, interceptor2)); + } + }; + } + } + + TelemetrySupportProvider build() { + if (telemetryEnabled) { + return new TelemetrySupportProvider(pathToClientSendingInterceptor, pathToServerSendingInterceptor, + pathToClientErrorInterceptor, pathToServerErrorInterceptor, serverEndpointDecorator, + clientEndpointDecorator, pathToClientConnectionInterceptor, pathToServerConnectionInterceptor); + } + return new TelemetrySupportProvider(); + } + +} 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 new file mode 100644 index 0000000000000..cd472e7d55cfb --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TelemetryWebSocketEndpointContext.java @@ -0,0 +1,13 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +import java.util.Map; + +import io.quarkus.websockets.next.runtime.WebSocketConnectionBase; +import io.quarkus.websockets.next.runtime.WebSocketEndpoint; + +/** + * Data carrier used to instantiate {@link TelemetrySupport}. + */ +record TelemetryWebSocketEndpointContext(WebSocketEndpoint endpoint, WebSocketConnectionBase connection, String path, + Map contextData) { +} 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 new file mode 100644 index 0000000000000..e839bda88b74f --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesBuilderCustomizer.java @@ -0,0 +1,77 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +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.SERVER_CONNECTION_OPENED; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_OPENED_ERROR; +import static io.quarkus.websockets.next.runtime.telemetry.TracesConnectionInterceptor.CONNECTION_OPENED_SPAN_CTX; + +import java.util.function.Consumer; +import java.util.function.Function; + +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.Tracer; +import io.quarkus.arc.Arc; +import io.quarkus.websockets.next.WebSocketClientConnection; +import io.quarkus.websockets.next.WebSocketConnection; +import io.quarkus.websockets.next.WebSocketsClientRuntimeConfig; +import io.quarkus.websockets.next.WebSocketsServerRuntimeConfig; +import io.quarkus.websockets.next.runtime.WebSocketEndpoint; + +/** + * Installs traces support into the WebSockets extension. + */ +public final class TracesBuilderCustomizer implements Consumer { + + @Override + public void accept(TelemetrySupportProviderBuilder builder) { + var container = Arc.container(); + + var serverTracesEnabled = container.instance(WebSocketsServerRuntimeConfig.class).get().telemetry().tracesEnabled(); + var clientTracesEnabled = container.instance(WebSocketsClientRuntimeConfig.class).get().telemetry().tracesEnabled(); + if (serverTracesEnabled || clientTracesEnabled) { + final Tracer tracer = container.instance(Tracer.class).get(); + if (serverTracesEnabled) { + addServerTracesSupport(builder, tracer); + } + if (clientTracesEnabled) { + addClientTracesSupport(builder, tracer); + } + } + } + + private static void addServerTracesSupport(TelemetrySupportProviderBuilder builder, Tracer tracer) { + builder.serverEndpointDecorator(new Function<>() { + @Override + public WebSocketEndpoint apply(TelemetryWebSocketEndpointContext ctx) { + var onOpenSpanCtx = (SpanContext) ctx.contextData().get(CONNECTION_OPENED_SPAN_CTX); + return new TracesWebSocketEndpointWrapper(ctx.endpoint(), tracer, (WebSocketConnection) ctx.connection(), + onOpenSpanCtx, ctx.path()); + } + }); + builder.pathToServerConnectionInterceptor(new Function<>() { + @Override + public ConnectionInterceptor apply(String path) { + return new TracesConnectionInterceptor(tracer, SERVER_CONNECTION_OPENED, SERVER_CONNECTION_OPENED_ERROR, path); + } + }); + } + + private static void addClientTracesSupport(TelemetrySupportProviderBuilder builder, Tracer tracer) { + builder.clientEndpointDecorator(new Function<>() { + @Override + public WebSocketEndpoint apply(TelemetryWebSocketEndpointContext ctx) { + var onOpenSpanCtx = (SpanContext) ctx.contextData().get(CONNECTION_OPENED_SPAN_CTX); + return new TracesWebSocketEndpointWrapper(ctx.endpoint(), tracer, (WebSocketClientConnection) ctx.connection(), + onOpenSpanCtx, ctx.path()); + } + }); + builder.pathToClientConnectionInterceptor(new Function<>() { + @Override + public ConnectionInterceptor apply(String path) { + return new TracesConnectionInterceptor(tracer, CLIENT_CONNECTION_OPENED, CLIENT_CONNECTION_OPENED_ERROR, path); + } + }); + } + +} 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 new file mode 100644 index 0000000000000..39daa1d3978b1 --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesConnectionInterceptor.java @@ -0,0 +1,64 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.CONNECTION_FAILURE_ATTR_KEY; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.URI_ATTR_KEY; + +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.Tracer; + +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 String connectionOpenedSpanName; + private final String connectionOpeningFailedSpanName; + private final String path; + private final Map contextData; + + TracesConnectionInterceptor(Tracer tracer, String connectionOpenedSpanName, String connectionOpeningFailedSpanName, + String path) { + this.tracer = tracer; + this.connectionOpenedSpanName = connectionOpenedSpanName; + this.connectionOpeningFailedSpanName = connectionOpeningFailedSpanName; + this.path = path; + this.contextData = new HashMap<>(); + } + + @Override + public void connectionOpened() { + var span = tracer.spanBuilder(connectionOpenedSpanName) + .addLink(previousSpanContext()) + .setAttribute(URI_ATTR_KEY, path) + .startSpan(); + contextData.put(CONNECTION_OPENED_SPAN_CTX, span.getSpanContext()); + span.end(); + } + + @Override + public void connectionOpeningFailed(Throwable cause) { + tracer.spanBuilder(connectionOpeningFailedSpanName) + .addLink((SpanContext) contextData.get(CONNECTION_OPENED_SPAN_CTX)) + .setAttribute(URI_ATTR_KEY, path) + .setAttribute(CONNECTION_FAILURE_ATTR_KEY, cause.getMessage()) + .startSpan() + .end(); + } + + @Override + public Map getContextData() { + return contextData; + } + + private static SpanContext previousSpanContext() { + var span = Span.current(); + if (span.getSpanContext().isValid()) { + return span.getSpanContext(); + } + return null; + } +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesWebSocketEndpointWrapper.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesWebSocketEndpointWrapper.java new file mode 100644 index 0000000000000..2624397496e52 --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/TracesWebSocketEndpointWrapper.java @@ -0,0 +1,78 @@ +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.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 io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.SERVER_CONNECTION_CLOSED; +import static io.quarkus.websockets.next.runtime.telemetry.TelemetryConstants.URI_ATTR_KEY; + +import java.util.function.Function; + +import io.opentelemetry.api.trace.SpanContext; +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.vertx.core.Future; + +/** + * {@link WebSocketEndpoint} wrapper that produces OpenTelemetry spans for closed connection. + */ +final class TracesWebSocketEndpointWrapper extends AbstractWebSocketEndpointWrapper { + + /** + * 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 String connectionClosedSpanName; + + TracesWebSocketEndpointWrapper(WebSocketEndpoint delegate, Tracer tracer, WebSocketConnection connection, + SpanContext onOpenSpanContext, String path) { + super(delegate); + this.tracer = tracer; + this.onOpenSpanContext = onOpenSpanContext; + this.connectionId = connection.id(); + this.targetIdKey = CONNECTION_ENDPOINT_ATTR_KEY; + this.targetIdValue = connection.endpointId(); + this.path = path; + this.connectionClosedSpanName = SERVER_CONNECTION_CLOSED; + } + + TracesWebSocketEndpointWrapper(WebSocketEndpoint delegate, Tracer tracer, WebSocketClientConnection connection, + SpanContext onOpenSpanContext, String path) { + super(delegate); + this.tracer = tracer; + this.onOpenSpanContext = onOpenSpanContext; + this.connectionId = connection.id(); + this.targetIdKey = CONNECTION_CLIENT_ATTR_KEY; + this.targetIdValue = connection.clientId(); + this.path = path; + this.connectionClosedSpanName = CLIENT_CONNECTION_CLOSED; + } + + @Override + public Future onClose() { + return delegate.onClose().map(new Function() { + @Override + public Void apply(Void unused) { + tracer.spanBuilder(connectionClosedSpanName) + .addLink(onOpenSpanContext) + .setAttribute(CONNECTION_ID_ATTR_KEY, connectionId) + .setAttribute(URI_ATTR_KEY, path) + .setAttribute(targetIdKey, targetIdValue) + .startSpan() + .end(); + return null; + } + }); + } +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/WebSocketTelemetryRecorder.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/WebSocketTelemetryRecorder.java new file mode 100644 index 0000000000000..17e411d4c0ca6 --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/telemetry/WebSocketTelemetryRecorder.java @@ -0,0 +1,31 @@ +package io.quarkus.websockets.next.runtime.telemetry; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class WebSocketTelemetryRecorder { + + public RuntimeValue createEmptyTelemetrySupportProvider() { + return new RuntimeValue<>(new TelemetrySupportProvider()); + } + + public Supplier createTelemetrySupportProvider( + List> builderCustomizers) { + return new Supplier<>() { + @Override + public TelemetrySupportProvider get() { + var builder = new TelemetrySupportProviderBuilder(); + for (Consumer customizer : builderCustomizers) { + customizer.accept(builder); + } + return builder.build(); + } + }; + } + +}