From f86312e2776fc9a7cb7cc0a0cfb098aa4b9364c7 Mon Sep 17 00:00:00 2001 From: Javier Salinas Date: Fri, 19 Nov 2021 20:22:10 +0100 Subject: [PATCH] Support ratpack functional tests (#4605) * Support manual initialization of OpenTelemetryServerHandler for ratpack functional tests * Use getters to do not expose opentelemetry implementations of ExecInterceptor and Handlers * Update instrumentation/ratpack-1.4/library/build.gradle.kts Co-authored-by: Mateusz Rzeszutek * Make OpenTelemtryServerHandler public to be binded from Guice and use directly in Ratpack Chain * Add documentation to getters methods to support Ratpack Registry bindings * Fix checkstyle in javadoc Co-authored-by: Mateusz Rzeszutek --- .../ratpack-1.4/library/build.gradle.kts | 1 + .../ratpack/OpenTelemetryServerHandler.java | 2 +- .../ratpack/RatpackTracing.java | 11 ++ .../ratpack/RatpackFunctionalTest.groovy | 35 ++++ .../RatpackServerApplicationTest.groovy | 117 +++++++++++++ .../ratpack/server/RatpackServerTest.groovy | 160 ++++++++++++++++++ 6 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 instrumentation/ratpack-1.4/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/RatpackFunctionalTest.groovy create mode 100644 instrumentation/ratpack-1.4/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/server/RatpackServerApplicationTest.groovy create mode 100644 instrumentation/ratpack-1.4/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/server/RatpackServerTest.groovy diff --git a/instrumentation/ratpack-1.4/library/build.gradle.kts b/instrumentation/ratpack-1.4/library/build.gradle.kts index 47690306fb..5d42899595 100644 --- a/instrumentation/ratpack-1.4/library/build.gradle.kts +++ b/instrumentation/ratpack-1.4/library/build.gradle.kts @@ -7,6 +7,7 @@ dependencies { library("io.ratpack:ratpack-core:1.4.0") testImplementation(project(":instrumentation:ratpack-1.4:testing")) + testLibrary("io.ratpack:ratpack-guice:1.4.0") if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_11)) { testImplementation("com.sun.activation:jakarta.activation:1.2.2") diff --git a/instrumentation/ratpack-1.4/library/src/main/java/io/opentelemetry/instrumentation/ratpack/OpenTelemetryServerHandler.java b/instrumentation/ratpack-1.4/library/src/main/java/io/opentelemetry/instrumentation/ratpack/OpenTelemetryServerHandler.java index 0ba650aa28..8930ad5b2a 100644 --- a/instrumentation/ratpack-1.4/library/src/main/java/io/opentelemetry/instrumentation/ratpack/OpenTelemetryServerHandler.java +++ b/instrumentation/ratpack-1.4/library/src/main/java/io/opentelemetry/instrumentation/ratpack/OpenTelemetryServerHandler.java @@ -15,7 +15,7 @@ import ratpack.handling.Handler; import ratpack.http.Request; import ratpack.http.Response; -final class OpenTelemetryServerHandler implements Handler { +public final class OpenTelemetryServerHandler implements Handler { private final Instrumenter instrumenter; diff --git a/instrumentation/ratpack-1.4/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackTracing.java b/instrumentation/ratpack-1.4/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackTracing.java index 2e985cae5d..2b9de88c31 100644 --- a/instrumentation/ratpack-1.4/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackTracing.java +++ b/instrumentation/ratpack-1.4/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackTracing.java @@ -7,6 +7,7 @@ package io.opentelemetry.instrumentation.ratpack; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import ratpack.exec.ExecInterceptor; import ratpack.handling.HandlerDecorator; import ratpack.http.Request; import ratpack.http.Response; @@ -46,6 +47,16 @@ public final class RatpackTracing { serverHandler = new OpenTelemetryServerHandler(serverInstrumenter); } + /** Returns instance of {@link OpenTelemetryServerHandler} to support Ratpack Registry binding. */ + public OpenTelemetryServerHandler getOpenTelemetryServerHandler() { + return serverHandler; + } + + /** Returns instance of {@link ExecInterceptor} to support Ratpack Registry binding. */ + public ExecInterceptor getOpenTelemetryExecInterceptor() { + return OpenTelemetryExecInterceptor.INSTANCE; + } + /** Configures the {@link RegistrySpec} with OpenTelemetry. */ public void configureServerRegistry(RegistrySpec registry) { registry.add(HandlerDecorator.prepend(serverHandler)); diff --git a/instrumentation/ratpack-1.4/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/RatpackFunctionalTest.groovy b/instrumentation/ratpack-1.4/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/RatpackFunctionalTest.groovy new file mode 100644 index 0000000000..6d4bf8fc83 --- /dev/null +++ b/instrumentation/ratpack-1.4/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/RatpackFunctionalTest.groovy @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ratpack + + +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter +import io.opentelemetry.sdk.trace.export.SpanExporter +import ratpack.impose.ForceDevelopmentImposition +import ratpack.impose.ImpositionsSpec +import ratpack.impose.UserRegistryImposition +import ratpack.registry.Registry +import ratpack.test.MainClassApplicationUnderTest + +class RatpackFunctionalTest extends MainClassApplicationUnderTest { + + Registry registry + @Lazy InMemorySpanExporter spanExporter = registry.get(SpanExporter) as InMemorySpanExporter + + RatpackFunctionalTest(Class mainClass) { + super(mainClass) + getAddress() + } + + @Override + void addImpositions(ImpositionsSpec impositions) { + impositions.add(ForceDevelopmentImposition.of(false)) + impositions.add(UserRegistryImposition.of { r -> + registry = r + registry + }) + } +} diff --git a/instrumentation/ratpack-1.4/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/server/RatpackServerApplicationTest.groovy b/instrumentation/ratpack-1.4/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/server/RatpackServerApplicationTest.groovy new file mode 100644 index 0000000000..a867322a25 --- /dev/null +++ b/instrumentation/ratpack-1.4/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/server/RatpackServerApplicationTest.groovy @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ratpack.server + +import com.google.inject.AbstractModule +import com.google.inject.Provides +import groovy.transform.CompileStatic +import io.opentelemetry.api.OpenTelemetry +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.instrumentation.ratpack.OpenTelemetryServerHandler +import io.opentelemetry.instrumentation.ratpack.RatpackFunctionalTest +import io.opentelemetry.instrumentation.ratpack.RatpackTracing +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter +import io.opentelemetry.sdk.trace.SdkTracerProvider +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor +import io.opentelemetry.sdk.trace.export.SpanExporter +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import ratpack.exec.ExecInterceptor +import ratpack.guice.Guice +import ratpack.server.RatpackServer +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +import javax.inject.Singleton + +class RatpackServerApplicationTest extends Specification { + + def app = new RatpackFunctionalTest(RatpackApp) + + def "add span on handlers"() { + expect: + app.test { httpClient -> "hi-foo" == httpClient.get("foo").body.text } + + new PollingConditions().eventually { + def spanData = app.spanExporter.finishedSpanItems.find { it.name == "/foo" } + def attributes = spanData.attributes.asMap() + + spanData.kind == SpanKind.SERVER + attributes[SemanticAttributes.HTTP_ROUTE] == "/foo" + attributes[SemanticAttributes.HTTP_TARGET] == "/foo" + attributes[SemanticAttributes.HTTP_METHOD] == "GET" + attributes[SemanticAttributes.HTTP_STATUS_CODE] == 200L + } + } + + def "ignore handlers before OpenTelemetryServerHandler"() { + expect: + app.test { httpClient -> "ignored" == httpClient.get("ignore").body.text } + + new PollingConditions(initialDelay: 0.1, timeout: 0.3).eventually { + !app.spanExporter.finishedSpanItems.any { it.name == "/ignore" } + } + } +} + + +@CompileStatic +class OpenTelemetryModule extends AbstractModule { + @Override + protected void configure() { + bind(SpanExporter).toInstance(InMemorySpanExporter.create()) + } + + @Singleton + @Provides + RatpackTracing ratpackTracing(OpenTelemetry openTelemetry) { + return RatpackTracing.create(openTelemetry) + } + + @Singleton + @Provides + OpenTelemetryServerHandler ratpackServerHandler(RatpackTracing ratpackTracing) { + return ratpackTracing.getOpenTelemetryServerHandler() + } + + @Singleton + @Provides + ExecInterceptor ratpackExecInterceptor(RatpackTracing ratpackTracing) { + return ratpackTracing.getOpenTelemetryExecInterceptor() + } + + @Provides + @Singleton + OpenTelemetry providesOpenTelemetry(SpanExporter spanExporter) { + def tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build() + return OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build() + } +} + +@CompileStatic +class RatpackApp { + + static void main(String... args) { + RatpackServer.start { server -> + server + .registry( + Guice.registry { bindings -> + bindings + .module(OpenTelemetryModule) + } + ) + .handlers { chain -> + chain + .get("ignore") { ctx -> ctx.render("ignored") } + .all(OpenTelemetryServerHandler) + .get("foo") { ctx -> ctx.render("hi-foo") } + } + } + } +} + diff --git a/instrumentation/ratpack-1.4/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/server/RatpackServerTest.groovy b/instrumentation/ratpack-1.4/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/server/RatpackServerTest.groovy new file mode 100644 index 0000000000..f60dcb4934 --- /dev/null +++ b/instrumentation/ratpack-1.4/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/server/RatpackServerTest.groovy @@ -0,0 +1,160 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ratpack.server + +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator +import io.opentelemetry.context.propagation.ContextPropagators +import io.opentelemetry.instrumentation.ratpack.RatpackTracing +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter +import io.opentelemetry.sdk.trace.SdkTracerProvider +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import ratpack.exec.Blocking +import ratpack.registry.Registry +import ratpack.test.embed.EmbeddedApp +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +class RatpackServerTest extends Specification { + + def spanExporter = InMemorySpanExporter.create() + def tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build() + + def openTelemetry = OpenTelemetrySdk.builder() + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .setTracerProvider(tracerProvider).build() + + def ratpackTracing = RatpackTracing.create(openTelemetry) + + def cleanup() { + spanExporter.reset() + } + + def "add span on handlers"() { + given: + def app = EmbeddedApp.of { spec -> + spec.registry { Registry.of { ratpackTracing.configureServerRegistry(it) } } + spec.handlers { chain -> + chain.get("foo") { ctx -> ctx.render("hi-foo") } + } + } + + when: + app.test { httpClient -> "hi-foo" == httpClient.get("foo").body.text } + + then: + new PollingConditions().eventually { + def spanData = spanExporter.finishedSpanItems.find { it.name == "/foo" } + def attributes = spanData.attributes.asMap() + + spanData.kind == SpanKind.SERVER + attributes[SemanticAttributes.HTTP_ROUTE] == "/foo" + attributes[SemanticAttributes.HTTP_TARGET] == "/foo" + attributes[SemanticAttributes.HTTP_METHOD] == "GET" + attributes[SemanticAttributes.HTTP_STATUS_CODE] == 200L + } + } + + def "propagate trace with instrumented async operations"() { + expect: + def app = EmbeddedApp.of { spec -> + spec.registry { Registry.of { ratpackTracing.configureServerRegistry(it) } } + spec.handlers { chain -> + chain.get("foo") { ctx -> + ctx.render("hi-foo") + Blocking.op { + def span = openTelemetry.getTracer("any-tracer").spanBuilder("a-span").startSpan() + span.makeCurrent().withCloseable { + span.addEvent("an-event") + span.end() + } + }.then() + } + } + } + + app.test { httpClient -> + "hi-foo" == httpClient.get("foo").body.text + + new PollingConditions().eventually { + def spanData = spanExporter.finishedSpanItems.find { it.name == "/foo" } + def spanDataChild = spanExporter.finishedSpanItems.find { it.name == "a-span" } + + spanData.kind == SpanKind.SERVER + spanData.traceId == spanDataChild.traceId + spanDataChild.parentSpanId == spanData.spanId + spanDataChild.events.any { it.name == "an-event" } + + def attributes = spanData.attributes.asMap() + attributes[SemanticAttributes.HTTP_ROUTE] == "/foo" + attributes[SemanticAttributes.HTTP_TARGET] == "/foo" + attributes[SemanticAttributes.HTTP_METHOD] == "GET" + attributes[SemanticAttributes.HTTP_STATUS_CODE] == 200L + } + } + } + + def "propagate trace with instrumented async concurrent operations"() { + expect: + def app = EmbeddedApp.of { spec -> + spec.registry { Registry.of { ratpackTracing.configureServerRegistry(it) } } + spec.handlers { chain -> + chain.get("bar") { ctx -> + ctx.render("hi-bar") + Blocking.op { + def span = openTelemetry.getTracer("any-tracer").spanBuilder("another-span").startSpan() + span.makeCurrent().withCloseable { + span.addEvent("an-event") + span.end() + } + }.then() + } + chain.get("foo") { ctx -> + ctx.render("hi-foo") + Blocking.op { + def span = openTelemetry.getTracer("any-tracer").spanBuilder("a-span").startSpan() + span.makeCurrent().withCloseable { + span.addEvent("an-event") + span.end() + } + }.then() + } + } + } + + app.test { httpClient -> + "hi-foo" == httpClient.get("foo").body.text + "hi-bar" == httpClient.get("bar").body.text + new PollingConditions().eventually { + def spanData = spanExporter.finishedSpanItems.find { it.name == "/foo" } + def spanDataChild = spanExporter.finishedSpanItems.find { it.name == "a-span" } + + def spanData2 = spanExporter.finishedSpanItems.find { it.name == "/bar" } + def spanDataChild2 = spanExporter.finishedSpanItems.find { it.name == "another-span" } + + spanData.kind == SpanKind.SERVER + spanData.traceId == spanDataChild.traceId + spanDataChild.parentSpanId == spanData.spanId + spanDataChild.events.any { it.name == "an-event" } + + spanData2.kind == SpanKind.SERVER + spanData2.traceId == spanDataChild2.traceId + spanDataChild2.parentSpanId == spanData2.spanId + spanDataChild2.events.any { it.name == "an-event" } + + def attributes = spanData.attributes.asMap() + attributes[SemanticAttributes.HTTP_ROUTE] == "/foo" + attributes[SemanticAttributes.HTTP_TARGET] == "/foo" + attributes[SemanticAttributes.HTTP_METHOD] == "GET" + attributes[SemanticAttributes.HTTP_STATUS_CODE] == 200L + } + } + } +}