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 <mrzeszutek@splunk.com>

* 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 <mrzeszutek@splunk.com>
This commit is contained in:
Javier Salinas 2021-11-19 20:22:10 +01:00 committed by GitHub
parent bcae1b11d3
commit f86312e277
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 325 additions and 1 deletions

View File

@ -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")

View File

@ -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<Request, Response> instrumenter;

View File

@ -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));

View File

@ -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
})
}
}

View File

@ -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") }
}
}
}
}

View File

@ -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
}
}
}
}