Add library instrumentation for Ratpack server (#3749)

* Add Ratpack server library instrumentation

* Finish

* Back to 1.4

* Drift

* Cocaine

* Update instrumentation/ratpack-1.4/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackTracingBuilder.java

Co-authored-by: Mateusz Rzeszutek <mrzeszutek@splunk.com>

* Cleanup

Co-authored-by: Mateusz Rzeszutek <mrzeszutek@splunk.com>
This commit is contained in:
Anuraag Agrawal 2021-08-04 16:21:36 +09:00 committed by GitHub
parent 32351d0bab
commit e92ecc02bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1424 additions and 513 deletions

View File

@ -15,6 +15,8 @@ dependencies {
implementation(project(":instrumentation:netty:netty-4.1:javaagent")) implementation(project(":instrumentation:netty:netty-4.1:javaagent"))
testImplementation(project(":instrumentation:ratpack-1.4:testing"))
testLibrary("io.ratpack:ratpack-test:1.4.0") testLibrary("io.ratpack:ratpack-test:1.4.0")
if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_11)) { if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_11)) {

View File

@ -1,116 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import static io.opentelemetry.api.trace.SpanKind.INTERNAL
import static io.opentelemetry.api.trace.SpanKind.SERVER
import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
import io.opentelemetry.instrumentation.test.utils.PortUtils
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
import io.opentelemetry.testing.internal.armeria.client.WebClient
import ratpack.path.PathBinding
import ratpack.server.RatpackServer
import spock.lang.Shared
class RatpackOtherTest extends AgentInstrumentationSpecification {
@Shared
RatpackServer app = RatpackServer.start {
it.serverConfig {
it.port(PortUtils.findOpenPort())
it.address(InetAddress.getByName("localhost"))
}
it.handlers {
it.prefix("a") {
it.all {context ->
context.render(context.get(PathBinding).description)
}
}
it.prefix("b/::\\d+") {
it.all {context ->
context.render(context.get(PathBinding).description)
}
}
it.prefix("c/:val?") {
it.all {context ->
context.render(context.get(PathBinding).description)
}
}
it.prefix("d/:val") {
it.all {context ->
context.render(context.get(PathBinding).description)
}
}
it.prefix("e/:val?:\\d+") {
it.all {context ->
context.render(context.get(PathBinding).description)
}
}
it.prefix("f/:val:\\d+") {
it.all {context ->
context.render(context.get(PathBinding).description)
}
}
}
}
// Force HTTP/1 with h1c to prevent tracing of upgrade request.
@Shared
WebClient client = WebClient.of("h1c://localhost:${app.bindPort}")
def cleanupSpec() {
app.stop()
}
def "test bindings for #path"() {
when:
def resp = client.get(path).aggregate().join()
then:
resp.status().code() == 200
resp.contentUtf8() == route
assertTraces(1) {
trace(0, 2) {
span(0) {
name "/$route"
kind SERVER
hasNoParent()
attributes {
"${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
"${SemanticAttributes.NET_PEER_PORT.key}" Long
"${SemanticAttributes.HTTP_URL.key}" "http://localhost:${app.bindPort}/${path}"
"${SemanticAttributes.HTTP_METHOD.key}" "GET"
"${SemanticAttributes.HTTP_STATUS_CODE.key}" 200
"${SemanticAttributes.HTTP_FLAVOR.key}" "1.1"
"${SemanticAttributes.HTTP_USER_AGENT.key}" String
"${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1"
}
}
span(1) {
name "/$route"
kind INTERNAL
childOf span(0)
attributes {
}
}
}
}
where:
path | route
"a" | "a"
"b/123" | "b/::\\d+"
"c" | "c/:val?"
"c/123" | "c/:val?"
"c/foo" | "c/:val?"
"d/123" | "d/:val"
"d/foo" | "d/:val"
"e" | "e/:val?:\\d+"
"e/123" | "e/:val?:\\d+"
"e/foo" | "e/:val?:\\d+"
"f/123" | "f/:val:\\d+"
}
}

View File

@ -5,114 +5,12 @@
package server package server
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR import io.opentelemetry.instrumentation.ratpack.server.AbstractRatpackAsyncHttpServerTest
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION import io.opentelemetry.instrumentation.test.AgentTestTrait
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD import ratpack.server.RatpackServerSpec
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS
import ratpack.error.ServerErrorHandler
import ratpack.exec.Promise
import ratpack.server.RatpackServer
class RatpackAsyncHttpServerTest extends RatpackHttpServerTest {
class RatpackAsyncHttpServerTest extends AbstractRatpackAsyncHttpServerTest implements AgentTestTrait {
@Override @Override
RatpackServer startServer(int bindPort) { void configure(RatpackServerSpec serverSpec) {
def ratpack = RatpackServer.start {
it.serverConfig {
it.port(bindPort)
it.address(InetAddress.getByName("localhost"))
}
it.handlers {
it.register {
it.add(ServerErrorHandler, new TestErrorHandler())
}
it.prefix(SUCCESS.rawPath()) {
it.all {context ->
Promise.sync {
SUCCESS
} then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(endpoint.body)
}
}
}
}
it.prefix(INDEXED_CHILD.rawPath()) {
it.all {context ->
Promise.sync {
INDEXED_CHILD
} then {
controller(INDEXED_CHILD) {
INDEXED_CHILD.collectSpanAttributes { context.request.queryParams.get(it) }
context.response.status(INDEXED_CHILD.status).send()
}
}
}
}
it.prefix(QUERY_PARAM.rawPath()) {
it.all { context ->
Promise.sync {
QUERY_PARAM
} then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(context.request.query)
}
}
}
}
it.prefix(REDIRECT.rawPath()) {
it.all {context ->
Promise.sync {
REDIRECT
} then { endpoint ->
controller(endpoint) {
context.redirect(endpoint.body)
}
}
}
}
it.prefix(ERROR.rawPath()) {
it.all {context ->
Promise.sync {
ERROR
} then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(endpoint.body)
}
}
}
}
it.prefix(EXCEPTION.rawPath()) {
it.all {
Promise.sync {
EXCEPTION
} then { endpoint ->
controller(endpoint) {
throw new Exception(endpoint.body)
}
}
}
}
it.prefix("path/:id/param") {
it.all {context ->
Promise.sync {
PATH_PARAM
} then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(context.pathTokens.id)
}
}
}
}
}
}
assert ratpack.bindPort == bindPort
assert ratpack.bindHost == 'localhost'
return ratpack
} }
} }

View File

@ -5,170 +5,12 @@
package server package server
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR import io.opentelemetry.instrumentation.ratpack.server.AbstractRatpackForkedHttpServerTest
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION import io.opentelemetry.instrumentation.test.AgentTestTrait
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD import ratpack.server.RatpackServerSpec
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse
import io.opentelemetry.testing.internal.armeria.common.HttpMethod
import ratpack.error.ServerErrorHandler
import ratpack.exec.Execution
import ratpack.exec.Promise
import ratpack.exec.Result
import ratpack.exec.util.ParallelBatch
import ratpack.server.RatpackServer
class RatpackForkedHttpServerTest extends RatpackHttpServerTest {
class RatpackForkedHttpServerTest extends AbstractRatpackForkedHttpServerTest implements AgentTestTrait {
@Override @Override
RatpackServer startServer(int bindPort) { void configure(RatpackServerSpec serverSpec) {
def ratpack = RatpackServer.start {
it.serverConfig {
it.port(bindPort)
it.address(InetAddress.getByName("localhost"))
}
it.handlers {
it.register {
it.add(ServerErrorHandler, new TestErrorHandler())
}
it.prefix(SUCCESS.rawPath()) {
it.all {context ->
Promise.sync {
SUCCESS
}.fork().then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(endpoint.body)
}
}
}
}
it.prefix(INDEXED_CHILD.rawPath()) {
it.all {context ->
Promise.sync {
INDEXED_CHILD
}.fork().then {
controller(INDEXED_CHILD) {
INDEXED_CHILD.collectSpanAttributes { context.request.queryParams.get(it) }
context.response.status(INDEXED_CHILD.status).send()
}
}
}
}
it.prefix(QUERY_PARAM.rawPath()) {
it.all { context ->
Promise.sync {
QUERY_PARAM
}.fork().then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(context.request.query)
}
}
}
}
it.prefix(REDIRECT.rawPath()) {
it.all {context ->
Promise.sync {
REDIRECT
}.fork().then { endpoint ->
controller(endpoint) {
context.redirect(endpoint.body)
}
}
}
}
it.prefix(ERROR.rawPath()) {
it.all {context ->
Promise.sync {
ERROR
}.fork().then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(endpoint.body)
}
}
}
}
it.prefix(EXCEPTION.rawPath()) {
it.all {
Promise.sync {
EXCEPTION
}.fork().then { endpoint ->
controller(endpoint) {
throw new Exception(endpoint.body)
}
}
}
}
it.prefix("path/:id/param") {
it.all {context ->
Promise.sync {
PATH_PARAM
}.fork().then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(context.pathTokens.id)
}
}
}
}
it.prefix("fork_and_yieldAll") {
it.all {context ->
def promise = Promise.async { upstream ->
Execution.fork().start({
upstream.accept(Result.success(SUCCESS))
})
}
ParallelBatch.of(promise).yieldAll().flatMap { list ->
Promise.sync { list.get(0).value }
} then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(endpoint.body)
}
}
}
}
}
}
assert ratpack.bindPort == bindPort
assert ratpack.bindHost == 'localhost'
return ratpack
}
def "test fork and yieldAll"() {
setup:
def url = address.resolve("fork_and_yieldAll").toString()
url = url.replace("http://", "h1c://")
def request = AggregatedHttpRequest.of(HttpMethod.GET, url)
AggregatedHttpResponse response = client.execute(request).aggregate().join()
expect:
response.status().code() == SUCCESS.status
response.contentUtf8() == SUCCESS.body
assertTraces(1) {
trace(0, 3) {
span(0) {
name "/fork_and_yieldAll"
kind SpanKind.SERVER
hasNoParent()
}
span(1) {
name "/fork_and_yieldAll"
kind SpanKind.INTERNAL
childOf span(0)
}
span(2) {
name "controller"
kind SpanKind.INTERNAL
childOf span(1)
}
}
}
} }
} }

View File

@ -5,136 +5,13 @@
package server package server
import static io.opentelemetry.api.trace.SpanKind.INTERNAL
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS
import io.opentelemetry.api.trace.StatusCode import io.opentelemetry.instrumentation.ratpack.server.AbstractRatpackHttpServerTest
import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.AgentTestTrait
import io.opentelemetry.instrumentation.test.asserts.TraceAssert import ratpack.server.RatpackServerSpec
import io.opentelemetry.instrumentation.test.base.HttpServerTest
import io.opentelemetry.sdk.trace.data.SpanData
import ratpack.error.ServerErrorHandler
import ratpack.handling.Context
import ratpack.server.RatpackServer
class RatpackHttpServerTest extends HttpServerTest<RatpackServer> implements AgentTestTrait {
class RatpackHttpServerTest extends AbstractRatpackHttpServerTest implements AgentTestTrait {
@Override @Override
RatpackServer startServer(int bindPort) { void configure(RatpackServerSpec serverSpec) {
def ratpack = RatpackServer.start {
it.serverConfig {
it.port(bindPort)
it.address(InetAddress.getByName("localhost"))
}
it.handlers {
it.register {
it.add(ServerErrorHandler, new TestErrorHandler())
}
it.prefix(SUCCESS.rawPath()) {
it.all {context ->
controller(SUCCESS) {
context.response.status(SUCCESS.status).send(SUCCESS.body)
}
}
}
it.prefix(INDEXED_CHILD.rawPath()) {
it.all {context ->
controller(INDEXED_CHILD) {
INDEXED_CHILD.collectSpanAttributes { context.request.queryParams.get(it) }
context.response.status(INDEXED_CHILD.status).send()
}
}
}
it.prefix(QUERY_PARAM.rawPath()) {
it.all { context ->
controller(QUERY_PARAM) {
context.response.status(QUERY_PARAM.status).send(context.request.query)
}
}
}
it.prefix(REDIRECT.rawPath()) {
it.all {context ->
controller(REDIRECT) {
context.redirect(REDIRECT.body)
}
}
}
it.prefix(ERROR.rawPath()) {
it.all {context ->
controller(ERROR) {
context.response.status(ERROR.status).send(ERROR.body)
}
}
}
it.prefix(EXCEPTION.rawPath()) {
it.all {
controller(EXCEPTION) {
throw new Exception(EXCEPTION.body)
}
}
}
it.prefix("path/:id/param") {
it.all {context ->
controller(PATH_PARAM) {
context.response.status(PATH_PARAM.status).send(context.pathTokens.id)
}
}
}
}
}
assert ratpack.bindPort == bindPort
return ratpack
}
static class TestErrorHandler implements ServerErrorHandler {
@Override
void error(Context context, Throwable throwable) throws Exception {
context.response.status(500).send(throwable.message)
}
}
@Override
void stopServer(RatpackServer server) {
server.stop()
}
@Override
boolean hasHandlerSpan(ServerEndpoint endpoint) {
true
}
@Override
boolean testPathParam() {
true
}
@Override
boolean testConcurrency() {
true
}
@Override
void handlerSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) {
trace.span(index) {
name endpoint.status == 404 ? "/" : endpoint == PATH_PARAM ? "/path/:id/param" : endpoint.path
kind INTERNAL
childOf((SpanData) parent)
if (endpoint == EXCEPTION) {
status StatusCode.ERROR
errorEvent(Exception, EXCEPTION.body)
}
}
}
@Override
String expectedServerSpanName(ServerEndpoint endpoint) {
return endpoint.status == 404 ? "/" : endpoint == PATH_PARAM ? "/path/:id/param" : endpoint.path
} }
} }

View File

@ -0,0 +1,21 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package server
import io.opentelemetry.instrumentation.ratpack.server.AbstractRatpackRoutesTest
import io.opentelemetry.instrumentation.test.AgentTestTrait
import ratpack.server.RatpackServerSpec
class RatpackRoutesTest extends AbstractRatpackRoutesTest implements AgentTestTrait {
@Override
void configure(RatpackServerSpec serverSpec) {
}
@Override
boolean hasHandlerSpan() {
return true
}
}

View File

@ -0,0 +1,17 @@
plugins {
id("otel.library-instrumentation")
id("otel.nullaway-conventions")
}
dependencies {
library("io.ratpack:ratpack-core:1.4.0")
testImplementation(project(":instrumentation:ratpack-1.4:testing"))
if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_11)) {
testImplementation("com.sun.activation:jakarta.activation:1.2.2")
}
}
// Requires old Guava. Can't use enforcedPlatform since predates BOM
configurations.testRuntimeClasspath.resolutionStrategy.force("com.google.guava:guava:19.0")

View File

@ -0,0 +1,57 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import ratpack.exec.ExecInterceptor;
import ratpack.exec.Execution;
import ratpack.func.Block;
final class OpenTelemetryExecInterceptor implements ExecInterceptor {
static final ExecInterceptor INSTANCE = new OpenTelemetryExecInterceptor();
@Override
public void intercept(Execution execution, ExecType type, Block continuation) throws Exception {
Context otelCtx = execution.maybeGet(Context.class).orElse(null);
if (otelCtx == null) {
// There is no OTel Context yet meaning this is the beginning of an Execution, before running
// the handler chain, which includes OpenTelemetryServerHandler. Run the chain.
executeHandlerChainAndThenCloseScope(execution, continuation);
} else {
// Execution already has a context, this is an asynchronous resumption and we need to make
// the context current.
executeContinuationWithContext(continuation, otelCtx);
}
}
private static void executeHandlerChainAndThenCloseScope(Execution execution, Block continuation)
throws Exception {
try {
continuation.execute();
} finally {
// The handler chain, including OpenTelemetryServerHandler, has finished and we are about
// to unbind the Execution from its thread. As such, we need to make sure to close the
// thread-local Scope that was created by OpenTelemetryServerHandler. The Execution still
// has an OTel Context, so if it happens to resume because the user used an asynchronous
// flow, the interceptor will run again and instead make the context current by
// calling executeContinuationWithContext.
Scope scope = execution.maybeGet(Scope.class).orElse(null);
if (scope != null) {
scope.close();
execution.remove(Scope.class);
}
}
}
private static void executeContinuationWithContext(Block continuation, Context otelCtx)
throws Exception {
try (Scope ignored = otelCtx.makeCurrent()) {
continuation.execute();
}
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.opentelemetry.instrumentation.ratpack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ratpack.error.ClientErrorHandler;
import ratpack.error.ServerErrorHandler;
import ratpack.handling.Context;
// Copied from
// https://github.com/ratpack/ratpack/blob/master/ratpack-core/src/main/java/ratpack/core/error/internal/DefaultProductionErrorHandler.java
// since it is internal and has had breaking changes.
final class OpenTelemetryFallbackErrorHandler implements ClientErrorHandler, ServerErrorHandler {
static final OpenTelemetryFallbackErrorHandler INSTANCE = new OpenTelemetryFallbackErrorHandler();
private static final Logger logger =
LoggerFactory.getLogger(OpenTelemetryFallbackErrorHandler.class);
OpenTelemetryFallbackErrorHandler() {}
@Override
public void error(Context context, int statusCode) {
if (logger.isWarnEnabled()) {
WarnOnce.execute();
logger.warn(getMsg(ClientErrorHandler.class, "client error", context));
}
context.getResponse().status(statusCode).send();
}
@Override
public void error(Context context, Throwable throwable) {
if (logger.isWarnEnabled()) {
WarnOnce.execute();
logger.warn(getMsg(ServerErrorHandler.class, "server error", context) + "\n", throwable);
}
context.getResponse().status(500).send();
}
private static String getMsg(Class<?> handlerClass, String type, Context context) {
return "Default production error handler used to render "
+ type
+ ", please add a "
+ handlerClass.getName()
+ " instance to your application "
+ "(method: "
+ context.getRequest().getMethod()
+ ", uri: "
+ context.getRequest().getRawUri()
+ ")";
}
private static class WarnOnce {
static {
logger.warn(
"Logging error using OpenTelemetryFallbackErrorHandler. This indicates "
+ "OpenTelemetry could not find a registered error handler which is not expected. "
+ "Log messages will only be outputed to console.");
}
// Warned once in static initializer, this is just to trigger classload.
static void execute() {}
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack;
import ratpack.error.ServerErrorHandler;
import ratpack.handling.Context;
final class OpenTelemetryServerErrorHandler implements ServerErrorHandler {
static final ServerErrorHandler INSTANCE = new OpenTelemetryServerErrorHandler();
private OpenTelemetryServerErrorHandler() {}
@Override
public void error(Context context, Throwable throwable) throws Exception {
context
.getExecution()
.add(
OpenTelemetryServerHandler.ErrorHolder.class,
new OpenTelemetryServerHandler.ErrorHolder(throwable));
ServerErrorHandler delegate = OpenTelemetryFallbackErrorHandler.INSTANCE;
for (ServerErrorHandler errorHandler : context.getAll(ServerErrorHandler.class)) {
if (errorHandler != INSTANCE) {
delegate = errorHandler;
break;
}
}
delegate.error(context, throwable);
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import ratpack.error.ServerErrorHandler;
import ratpack.handling.Context;
import ratpack.handling.Handler;
import ratpack.http.Request;
import ratpack.http.Response;
final class OpenTelemetryServerHandler implements Handler {
private final Instrumenter<Request, Response> instrumenter;
OpenTelemetryServerHandler(Instrumenter<Request, Response> instrumenter) {
this.instrumenter = instrumenter;
}
@Override
public void handle(Context context) {
Request request = context.getRequest();
io.opentelemetry.context.Context parentOtelCtx = io.opentelemetry.context.Context.current();
if (!instrumenter.shouldStart(parentOtelCtx, request)) {
context.next();
return;
}
io.opentelemetry.context.Context otelCtx = instrumenter.start(parentOtelCtx, request);
context.getExecution().add(io.opentelemetry.context.Context.class, otelCtx);
context.onClose(
outcome -> {
// Route not available in beginning of request so handle it manually here.
String route = '/' + context.getPathBinding().getDescription();
Span span = Span.fromContext(otelCtx);
span.updateName(route);
span.setAttribute(SemanticAttributes.HTTP_ROUTE, route);
Throwable error =
context.getExecution().maybeGet(ErrorHolder.class).map(ErrorHolder::get).orElse(null);
instrumenter.end(otelCtx, outcome.getRequest(), context.getResponse(), error);
});
// An execution continues to execute synchronously until it is unbound from a thread. We need
// to make the context current here to make it available to the next handler (possibly user
// code) but close the scope at the end of the ExecInterceptor, which corresponds to when the
// execution is about to be unbound from the thread.
Scope scope = otelCtx.makeCurrent();
context.getExecution().add(Scope.class, scope);
// A user may have defined their own ServerErrorHandler, so we add ours to the Execution which
// has higher precedence.
context.getExecution().add(ServerErrorHandler.class, OpenTelemetryServerErrorHandler.INSTANCE);
context.next();
}
static final class ErrorHolder {
private final Throwable throwable;
ErrorHolder(Throwable throwable) {
this.throwable = throwable;
}
Throwable get() {
return throwable;
}
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack;
import io.opentelemetry.context.propagation.TextMapGetter;
import javax.annotation.Nullable;
import ratpack.http.Request;
final class RatpackGetter implements TextMapGetter<Request> {
RatpackGetter() {}
@Override
public Iterable<String> keys(Request request) {
return request.getHeaders().getNames();
}
@Nullable
@Override
public String get(@Nullable Request request, String key) {
if (request == null) {
return null;
}
return request.getHeaders().get(key);
}
}

View File

@ -0,0 +1,130 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import org.checkerframework.checker.nullness.qual.Nullable;
import ratpack.handling.Context;
import ratpack.http.Request;
import ratpack.http.Response;
import ratpack.server.PublicAddress;
final class RatpackHttpAttributesExtractor extends HttpAttributesExtractor<Request, Response> {
@Override
protected String method(Request request) {
return request.getMethod().getName();
}
@Override
@Nullable
protected String url(Request request) {
// TODO(anuraaga): We should probably just not fill this
// https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/3700
Context ratpackContext = request.get(Context.class);
if (ratpackContext == null) {
return null;
}
PublicAddress publicAddress = ratpackContext.get(PublicAddress.class);
if (publicAddress == null) {
return null;
}
return publicAddress
.builder()
.path(request.getPath())
.params(request.getQueryParams())
.build()
.toString();
}
@Override
protected String target(Request request) {
// Uri is the path + query string, not a full URL
return request.getUri();
}
@Override
@Nullable
protected String host(Request request) {
return null;
}
@Override
@Nullable
protected String route(Request request) {
// Ratpack route not available at the beginning of request.
return null;
}
@Override
@Nullable
protected String scheme(Request request) {
return null;
}
@Override
@Nullable
protected String userAgent(Request request) {
return request.getHeaders().get("user-agent");
}
@Override
@Nullable
protected Long requestContentLength(Request request, @Nullable Response response) {
return null;
}
@Override
@Nullable
protected Long requestContentLengthUncompressed(Request request, @Nullable Response response) {
return null;
}
@Override
@Nullable
protected String flavor(Request request, @Nullable Response response) {
switch (request.getProtocol()) {
case "HTTP/1.0":
return SemanticAttributes.HttpFlavorValues.HTTP_1_0;
case "HTTP/1.1":
return SemanticAttributes.HttpFlavorValues.HTTP_1_1;
case "HTTP/2.0":
return SemanticAttributes.HttpFlavorValues.HTTP_2_0;
default:
// fall through
}
return null;
}
@Override
@Nullable
protected String serverName(Request request, @Nullable Response response) {
return null;
}
@Override
@Nullable
protected String clientIp(Request request, @Nullable Response response) {
return null;
}
@Override
protected Integer statusCode(Request request, Response response) {
return response.getStatus().getCode();
}
@Override
@Nullable
protected Long responseContentLength(Request request, Response response) {
return null;
}
@Override
@Nullable
protected Long responseContentLengthUncompressed(Request request, Response response) {
return null;
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import ratpack.handling.HandlerDecorator;
import ratpack.http.Request;
import ratpack.http.Response;
import ratpack.registry.RegistrySpec;
/**
* Entrypoint for tracing Ratpack servers. To apply OpenTelemetry to a server, configure the {@link
* RegistrySpec} using {@link #configureServerRegistry(RegistrySpec)}.
*
* <pre>{@code
* RatpackTracing tracing = RatpackTracing.create(OpenTelemetrySdk.builder()
* ...
* .build());
* RatpackServer.start(server -> {
* server.registryOf(tracing::configureServerRegistry);
* server.handlers(chain -> ...);
* });
* }</pre>
*/
public final class RatpackTracing {
/** Returns a new {@link RatpackTracing} configured with the given {@link OpenTelemetry}. */
public static RatpackTracing create(OpenTelemetry openTelemetry) {
return newBuilder(openTelemetry).build();
}
/**
* Returns a new {@link RatpackTracingBuilder} configured with the given {@link OpenTelemetry}.
*/
public static RatpackTracingBuilder newBuilder(OpenTelemetry openTelemetry) {
return new RatpackTracingBuilder(openTelemetry);
}
private final OpenTelemetryServerHandler serverHandler;
RatpackTracing(Instrumenter<Request, Response> serverInstrumenter) {
serverHandler = new OpenTelemetryServerHandler(serverInstrumenter);
}
/** Configures the {@link RegistrySpec} with OpenTelemetry. */
public void configureServerRegistry(RegistrySpec registry) {
registry.add(HandlerDecorator.prepend(serverHandler));
registry.add(OpenTelemetryExecInterceptor.INSTANCE);
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor;
import io.opentelemetry.instrumentation.ratpack.internal.RatpackNetAttributesExtractor;
import java.util.ArrayList;
import java.util.List;
import ratpack.http.Request;
import ratpack.http.Response;
/** A builder for {@link RatpackTracing}. */
public final class RatpackTracingBuilder {
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.ratpack-1.4";
private final OpenTelemetry openTelemetry;
private final List<AttributesExtractor<? super Request, ? super Response>> additionalExtractors =
new ArrayList<>();
RatpackTracingBuilder(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}
/**
* Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented
* items. The {@link AttributesExtractor} will be executed after all default extractors.
*/
public RatpackTracingBuilder addAttributeExtractor(
AttributesExtractor<? super Request, ? super Response> attributesExtractor) {
additionalExtractors.add(attributesExtractor);
return this;
}
/** Returns a new {@link RatpackTracing} with the configuration of this builder. */
public RatpackTracing build() {
RatpackNetAttributesExtractor netAttributes = new RatpackNetAttributesExtractor();
RatpackHttpAttributesExtractor httpAttributes = new RatpackHttpAttributesExtractor();
InstrumenterBuilder<Request, Response> builder =
Instrumenter.newBuilder(
openTelemetry, INSTRUMENTATION_NAME, HttpSpanNameExtractor.create(httpAttributes));
builder.setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributes));
builder.addAttributesExtractor(netAttributes);
builder.addAttributesExtractor(httpAttributes);
builder.addAttributesExtractors(additionalExtractors);
return new RatpackTracing(builder.newServerInstrumenter(new RatpackGetter()));
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack.internal;
import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import org.checkerframework.checker.nullness.qual.Nullable;
import ratpack.http.Request;
import ratpack.http.Response;
public final class RatpackNetAttributesExtractor extends NetAttributesExtractor<Request, Response> {
@Override
@Nullable
public String transport(Request request) {
return SemanticAttributes.NetTransportValues.IP_TCP;
}
@Override
@Nullable
public String peerName(Request request, @Nullable Response response) {
return null;
}
@Override
public Integer peerPort(Request request, @Nullable Response response) {
return request.getRemoteAddress().getPort();
}
@Override
@Nullable
public String peerIp(Request request, @Nullable Response response) {
return null;
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack.server
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.instrumentation.ratpack.RatpackTracing
import io.opentelemetry.instrumentation.test.LibraryTestTrait
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
import ratpack.server.RatpackServerSpec
class RatpackAsyncHttpServerTest extends AbstractRatpackAsyncHttpServerTest implements LibraryTestTrait {
@Override
void configure(RatpackServerSpec serverSpec) {
RatpackTracing tracing = RatpackTracing.create(openTelemetry)
serverSpec.registryOf {
tracing.configureServerRegistry(it)
}
}
@Override
boolean hasHandlerSpan(ServerEndpoint endpoint) {
false
}
@Override
List<AttributeKey<?>> extraAttributes() {
return [
SemanticAttributes.HTTP_ROUTE,
SemanticAttributes.HTTP_TARGET,
SemanticAttributes.NET_TRANSPORT,
]
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack.server
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.instrumentation.ratpack.RatpackTracing
import io.opentelemetry.instrumentation.test.LibraryTestTrait
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
import ratpack.server.RatpackServerSpec
class RatpackForkedHttpServerTest extends AbstractRatpackForkedHttpServerTest implements LibraryTestTrait {
@Override
void configure(RatpackServerSpec serverSpec) {
RatpackTracing tracing = RatpackTracing.create(openTelemetry)
serverSpec.registryOf {
tracing.configureServerRegistry(it)
}
}
@Override
boolean hasHandlerSpan(ServerEndpoint endpoint) {
false
}
@Override
List<AttributeKey<?>> extraAttributes() {
return [
SemanticAttributes.HTTP_ROUTE,
SemanticAttributes.HTTP_TARGET,
SemanticAttributes.NET_TRANSPORT,
]
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack.server
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.instrumentation.ratpack.RatpackTracing
import io.opentelemetry.instrumentation.test.LibraryTestTrait
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
import ratpack.server.RatpackServerSpec
class RatpackHttpServerTest extends AbstractRatpackHttpServerTest implements LibraryTestTrait {
@Override
void configure(RatpackServerSpec serverSpec) {
RatpackTracing tracing = RatpackTracing.create(openTelemetry)
serverSpec.registryOf {
tracing.configureServerRegistry(it)
}
}
@Override
boolean hasHandlerSpan(ServerEndpoint endpoint) {
false
}
@Override
List<AttributeKey<?>> extraAttributes() {
return [
SemanticAttributes.HTTP_ROUTE,
SemanticAttributes.HTTP_TARGET,
SemanticAttributes.NET_TRANSPORT,
]
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack.server
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.instrumentation.ratpack.RatpackTracing
import io.opentelemetry.instrumentation.test.LibraryTestTrait
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
import ratpack.server.RatpackServerSpec
class RatpackRoutesTest extends AbstractRatpackRoutesTest implements LibraryTestTrait {
@Override
void configure(RatpackServerSpec serverSpec) {
RatpackTracing tracing = RatpackTracing.create(openTelemetry)
serverSpec.registryOf {
tracing.configureServerRegistry(it)
}
}
@Override
boolean hasHandlerSpan() {
return false
}
@Override
List<AttributeKey<?>> extraAttributes() {
return [
SemanticAttributes.HTTP_ROUTE,
SemanticAttributes.HTTP_TARGET,
SemanticAttributes.NET_TRANSPORT,
]
}
}

View File

@ -0,0 +1,13 @@
plugins {
id("otel.java-conventions")
}
dependencies {
api(project(":testing-common"))
api("io.ratpack:ratpack-core:1.4.0")
implementation("org.codehaus.groovy:groovy-all")
implementation("io.opentelemetry:opentelemetry-api")
implementation("org.spockframework:spock-core")
}

View File

@ -0,0 +1,119 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack.server
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS
import ratpack.error.ServerErrorHandler
import ratpack.exec.Promise
import ratpack.server.RatpackServer
abstract class AbstractRatpackAsyncHttpServerTest extends AbstractRatpackHttpServerTest {
@Override
RatpackServer startServer(int bindPort) {
def ratpack = RatpackServer.start {
it.serverConfig {
it.port(bindPort)
it.address(InetAddress.getByName("localhost"))
}
it.handlers {
it.register {
it.add(ServerErrorHandler, new TestErrorHandler())
}
it.prefix(SUCCESS.rawPath()) {
it.all {context ->
Promise.sync {
SUCCESS
} then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(endpoint.body)
}
}
}
}
it.prefix(INDEXED_CHILD.rawPath()) {
it.all {context ->
Promise.sync {
INDEXED_CHILD
} then {
controller(INDEXED_CHILD) {
INDEXED_CHILD.collectSpanAttributes { context.request.queryParams.get(it) }
context.response.status(INDEXED_CHILD.status).send()
}
}
}
}
it.prefix(QUERY_PARAM.rawPath()) {
it.all { context ->
Promise.sync {
QUERY_PARAM
} then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(context.request.query)
}
}
}
}
it.prefix(REDIRECT.rawPath()) {
it.all {context ->
Promise.sync {
REDIRECT
} then { endpoint ->
controller(endpoint) {
context.redirect(endpoint.body)
}
}
}
}
it.prefix(ERROR.rawPath()) {
it.all {context ->
Promise.sync {
ERROR
} then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(endpoint.body)
}
}
}
}
it.prefix(EXCEPTION.rawPath()) {
it.all {
Promise.sync {
EXCEPTION
} then { endpoint ->
controller(endpoint) {
throw new Exception(endpoint.body)
}
}
}
}
it.prefix("path/:id/param") {
it.all {context ->
Promise.sync {
PATH_PARAM
} then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(context.pathTokens.id)
}
}
}
}
}
configure(it)
}
assert ratpack.bindPort == bindPort
assert ratpack.bindHost == 'localhost'
return ratpack
}
}

View File

@ -0,0 +1,183 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack.server
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse
import io.opentelemetry.testing.internal.armeria.common.HttpMethod
import ratpack.error.ServerErrorHandler
import ratpack.exec.Execution
import ratpack.exec.Promise
import ratpack.exec.Result
import ratpack.exec.util.ParallelBatch
import ratpack.server.RatpackServer
abstract class AbstractRatpackForkedHttpServerTest extends AbstractRatpackHttpServerTest {
@Override
RatpackServer startServer(int bindPort) {
def ratpack = RatpackServer.start {
it.serverConfig {
it.port(bindPort)
it.address(InetAddress.getByName("localhost"))
}
it.handlers {
it.register {
it.add(ServerErrorHandler, new TestErrorHandler())
}
it.prefix(SUCCESS.rawPath()) {
it.all {context ->
Promise.sync {
SUCCESS
}.fork().then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(endpoint.body)
}
}
}
}
it.prefix(INDEXED_CHILD.rawPath()) {
it.all {context ->
Promise.sync {
INDEXED_CHILD
}.fork().then {
controller(INDEXED_CHILD) {
INDEXED_CHILD.collectSpanAttributes { context.request.queryParams.get(it) }
context.response.status(INDEXED_CHILD.status).send()
}
}
}
}
it.prefix(QUERY_PARAM.rawPath()) {
it.all { context ->
Promise.sync {
QUERY_PARAM
}.fork().then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(context.request.query)
}
}
}
}
it.prefix(REDIRECT.rawPath()) {
it.all {context ->
Promise.sync {
REDIRECT
}.fork().then { endpoint ->
controller(endpoint) {
context.redirect(endpoint.body)
}
}
}
}
it.prefix(ERROR.rawPath()) {
it.all {context ->
Promise.sync {
ERROR
}.fork().then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(endpoint.body)
}
}
}
}
it.prefix(EXCEPTION.rawPath()) {
it.all {
Promise.sync {
EXCEPTION
}.fork().then { endpoint ->
controller(endpoint) {
throw new Exception(endpoint.body)
}
}
}
}
it.prefix("path/:id/param") {
it.all {context ->
Promise.sync {
PATH_PARAM
}.fork().then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(context.pathTokens.id)
}
}
}
}
it.prefix("fork_and_yieldAll") {
it.all {context ->
def promise = Promise.async { upstream ->
Execution.fork().start({
upstream.accept(Result.success(SUCCESS))
})
}
ParallelBatch.of(promise).yieldAll().flatMap { list ->
Promise.sync { list.get(0).value }
} then { endpoint ->
controller(endpoint) {
context.response.status(endpoint.status).send(endpoint.body)
}
}
}
}
}
configure(it)
}
assert ratpack.bindPort == bindPort
assert ratpack.bindHost == 'localhost'
return ratpack
}
def "test fork and yieldAll"() {
setup:
def url = address.resolve("fork_and_yieldAll").toString()
url = url.replace("http://", "h1c://")
def request = AggregatedHttpRequest.of(HttpMethod.GET, url)
AggregatedHttpResponse response = client.execute(request).aggregate().join()
expect:
response.status().code() == SUCCESS.status
response.contentUtf8() == SUCCESS.body
assertTraces(1) {
trace(0, 2 + (hasHandlerSpan(SUCCESS) ? 1 : 0)) {
span(0) {
name "/fork_and_yieldAll"
kind SpanKind.SERVER
hasNoParent()
}
if (hasHandlerSpan(SUCCESS)) {
span(1) {
name "/fork_and_yieldAll"
kind SpanKind.INTERNAL
childOf span(0)
}
span(2) {
name "controller"
kind SpanKind.INTERNAL
childOf span(1)
}
} else {
span(1) {
name "controller"
kind SpanKind.INTERNAL
childOf span(0)
}
}
}
}
}
}

View File

@ -0,0 +1,146 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack.server
import static io.opentelemetry.api.trace.SpanKind.INTERNAL
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS
import io.opentelemetry.api.trace.StatusCode
import io.opentelemetry.instrumentation.test.asserts.TraceAssert
import io.opentelemetry.instrumentation.test.base.HttpServerTest
import io.opentelemetry.sdk.trace.data.SpanData
import ratpack.error.ServerErrorHandler
import ratpack.handling.Context
import ratpack.server.RatpackServer
import ratpack.server.RatpackServerSpec
abstract class AbstractRatpackHttpServerTest extends HttpServerTest<RatpackServer> {
abstract void configure(RatpackServerSpec serverSpec)
@Override
RatpackServer startServer(int bindPort) {
def ratpack = RatpackServer.start {
it.serverConfig {
it.port(bindPort)
it.address(InetAddress.getByName("localhost"))
}
it.handlers {
it.register {
it.add(ServerErrorHandler, new TestErrorHandler())
}
it.prefix(SUCCESS.rawPath()) {
it.all {context ->
controller(SUCCESS) {
context.response.status(SUCCESS.status).send(SUCCESS.body)
}
}
}
it.prefix(INDEXED_CHILD.rawPath()) {
it.all {context ->
controller(INDEXED_CHILD) {
INDEXED_CHILD.collectSpanAttributes { context.request.queryParams.get(it) }
context.response.status(INDEXED_CHILD.status).send()
}
}
}
it.prefix(QUERY_PARAM.rawPath()) {
it.all { context ->
controller(QUERY_PARAM) {
context.response.status(QUERY_PARAM.status).send(context.request.query)
}
}
}
it.prefix(REDIRECT.rawPath()) {
it.all {context ->
controller(REDIRECT) {
context.redirect(REDIRECT.body)
}
}
}
it.prefix(ERROR.rawPath()) {
it.all {context ->
controller(ERROR) {
context.response.status(ERROR.status).send(ERROR.body)
}
}
}
it.prefix(EXCEPTION.rawPath()) {
it.all {
controller(EXCEPTION) {
throw new Exception(EXCEPTION.body)
}
}
}
it.prefix("path/:id/param") {
it.all {context ->
controller(PATH_PARAM) {
context.response.status(PATH_PARAM.status).send(context.pathTokens.id)
}
}
}
}
configure(it)
}
assert ratpack.bindPort == bindPort
return ratpack
}
// TODO(anuraaga): The default Ratpack error handler also returns a 500 which is all we test, so
// we don't actually have test coverage ensuring our instrumentation correctly delegates to this
// user registered handler.
static class TestErrorHandler implements ServerErrorHandler {
@Override
void error(Context context, Throwable throwable) throws Exception {
context.response.status(500).send(throwable.message)
}
}
@Override
void stopServer(RatpackServer server) {
server.stop()
}
@Override
boolean hasHandlerSpan(ServerEndpoint endpoint) {
true
}
@Override
boolean testPathParam() {
true
}
@Override
boolean testConcurrency() {
true
}
@Override
void handlerSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) {
trace.span(index) {
name endpoint.status == 404 ? "/" : endpoint == PATH_PARAM ? "/path/:id/param" : endpoint.path
kind INTERNAL
childOf((SpanData) parent)
if (endpoint == EXCEPTION) {
status StatusCode.ERROR
errorEvent(Exception, EXCEPTION.body)
}
}
}
@Override
String expectedServerSpanName(ServerEndpoint endpoint) {
return endpoint.status == 404 ? "/" : endpoint == PATH_PARAM ? "/path/:id/param" : endpoint.path
}
}

View File

@ -0,0 +1,171 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack.server
import static io.opentelemetry.api.trace.SpanKind.INTERNAL
import static io.opentelemetry.api.trace.SpanKind.SERVER
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.instrumentation.test.InstrumentationSpecification
import io.opentelemetry.instrumentation.test.utils.PortUtils
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
import io.opentelemetry.testing.internal.armeria.client.WebClient
import ratpack.path.PathBinding
import ratpack.server.RatpackServer
import ratpack.server.RatpackServerSpec
import spock.lang.Shared
import spock.lang.Unroll
@Unroll
abstract class AbstractRatpackRoutesTest extends InstrumentationSpecification {
abstract void configure(RatpackServerSpec serverSpec)
@Shared
RatpackServer app
// Force HTTP/1 with h1c to prevent tracing of upgrade request.
@Shared
WebClient client
def setupSpec() {
app = RatpackServer.start {
it.serverConfig {
it.port(PortUtils.findOpenPort())
it.address(InetAddress.getByName("localhost"))
}
it.handlers {
it.prefix("a") {
it.all { context ->
context.render(context.get(PathBinding).description)
}
}
it.prefix("b/::\\d+") {
it.all { context ->
context.render(context.get(PathBinding).description)
}
}
it.prefix("c/:val?") {
it.all { context ->
context.render(context.get(PathBinding).description)
}
}
it.prefix("d/:val") {
it.all { context ->
context.render(context.get(PathBinding).description)
}
}
it.prefix("e/:val?:\\d+") {
it.all { context ->
context.render(context.get(PathBinding).description)
}
}
it.prefix("f/:val:\\d+") {
it.all { context ->
context.render(context.get(PathBinding).description)
}
}
}
configure(it)
}
client = WebClient.of("h1c://localhost:${app.bindPort}")
}
def cleanupSpec() {
app.stop()
}
abstract boolean hasHandlerSpan()
List<AttributeKey<?>> extraAttributes() {
[]
}
def "test bindings for #path"() {
when:
def resp = client.get(path).aggregate().join()
then:
resp.status().code() == 200
resp.contentUtf8() == route
def extraAttributes = extraAttributes()
assertTraces(1) {
trace(0, 1 + (hasHandlerSpan() ? 1 : 0)) {
span(0) {
name "/$route"
kind SERVER
hasNoParent()
attributes {
"${SemanticAttributes.NET_PEER_IP.key}" { it == null || it == "127.0.0.1" }
"${SemanticAttributes.NET_PEER_PORT.key}" Long
"${SemanticAttributes.HTTP_URL.key}" "http://localhost:${app.bindPort}/${path}"
"${SemanticAttributes.HTTP_METHOD.key}" "GET"
"${SemanticAttributes.HTTP_STATUS_CODE.key}" 200
"${SemanticAttributes.HTTP_FLAVOR.key}" "1.1"
"${SemanticAttributes.HTTP_USER_AGENT.key}" String
"${SemanticAttributes.HTTP_CLIENT_IP.key}" { it == null || it == "127.0.0.1" }
if (extraAttributes.contains(SemanticAttributes.HTTP_HOST)) {
"${SemanticAttributes.HTTP_HOST}" "localhost:${app.bindPort}"
}
if (extraAttributes.contains(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH)) {
"${SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH}" Long
}
if (extraAttributes.contains(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH)) {
"${SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH}" Long
}
if (extraAttributes.contains(SemanticAttributes.HTTP_ROUTE)) {
// TODO(anuraaga): Revisit this when applying instrumenters to more libraries, Armeria
// currently reports '/*' which is a fallback route.
"${SemanticAttributes.HTTP_ROUTE}" String
}
if (extraAttributes.contains(SemanticAttributes.HTTP_SCHEME)) {
"${SemanticAttributes.HTTP_SCHEME}" "http"
}
if (extraAttributes.contains(SemanticAttributes.HTTP_SERVER_NAME)) {
"${SemanticAttributes.HTTP_SERVER_NAME}" String
}
if (extraAttributes.contains(SemanticAttributes.HTTP_TARGET)) {
"${SemanticAttributes.HTTP_TARGET}" "/$path"
}
if (extraAttributes.contains(SemanticAttributes.NET_PEER_NAME)) {
"${SemanticAttributes.NET_PEER_NAME}" "localhost"
}
if (extraAttributes.contains(SemanticAttributes.NET_TRANSPORT)) {
"${SemanticAttributes.NET_TRANSPORT}" IP_TCP
}
}
}
if (hasHandlerSpan()) {
span(1) {
name "/$route"
kind INTERNAL
childOf span(0)
attributes {
}
}
}
}
}
where:
path | route
"a" | "a"
"b/123" | "b/::\\d+"
"c" | "c/:val?"
"c/123" | "c/:val?"
"c/foo" | "c/:val?"
"d/123" | "d/:val"
"d/foo" | "d/:val"
"e" | "e/:val?:\\d+"
"e/123" | "e/:val?:\\d+"
"e/foo" | "e/:val?:\\d+"
"f/123" | "f/:val:\\d+"
}
}

View File

@ -260,6 +260,8 @@ include(":instrumentation:play-ws:play-ws-common:javaagent")
include(":instrumentation:play-ws:play-ws-testing") include(":instrumentation:play-ws:play-ws-testing")
include(":instrumentation:rabbitmq-2.7:javaagent") include(":instrumentation:rabbitmq-2.7:javaagent")
include(":instrumentation:ratpack-1.4:javaagent") include(":instrumentation:ratpack-1.4:javaagent")
include(":instrumentation:ratpack-1.4:library")
include(":instrumentation:ratpack-1.4:testing")
include(":instrumentation:reactor-3.1:javaagent") include(":instrumentation:reactor-3.1:javaagent")
include(":instrumentation:reactor-3.1:library") include(":instrumentation:reactor-3.1:library")
include(":instrumentation:reactor-3.1:testing") include(":instrumentation:reactor-3.1:testing")

View File

@ -608,6 +608,7 @@ abstract class HttpServerTest<SERVER> extends InstrumentationSpecification imple
} }
void indexedServerSpan(TraceAssert trace, Object parent, int requestId) { void indexedServerSpan(TraceAssert trace, Object parent, int requestId) {
def extraAttributes = extraAttributes()
ServerEndpoint endpoint = INDEXED_CHILD ServerEndpoint endpoint = INDEXED_CHILD
trace.span(1) { trace.span(1) {
name expectedServerSpanName(endpoint) name expectedServerSpanName(endpoint)
@ -622,6 +623,36 @@ abstract class HttpServerTest<SERVER> extends InstrumentationSpecification imple
"${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200
"${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1"
"${SemanticAttributes.HTTP_USER_AGENT.key}" TEST_USER_AGENT "${SemanticAttributes.HTTP_USER_AGENT.key}" TEST_USER_AGENT
if (extraAttributes.contains(SemanticAttributes.HTTP_HOST)) {
"${SemanticAttributes.HTTP_HOST}" "localhost:${port}"
}
if (extraAttributes.contains(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH)) {
"${SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH}" Long
}
if (extraAttributes.contains(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH)) {
"${SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH}" Long
}
if (extraAttributes.contains(SemanticAttributes.HTTP_ROUTE)) {
// TODO(anuraaga): Revisit this when applying instrumenters to more libraries, Armeria
// currently reports '/*' which is a fallback route.
"${SemanticAttributes.HTTP_ROUTE}" String
}
if (extraAttributes.contains(SemanticAttributes.HTTP_SCHEME)) {
"${SemanticAttributes.HTTP_SCHEME}" "http"
}
if (extraAttributes.contains(SemanticAttributes.HTTP_SERVER_NAME)) {
"${SemanticAttributes.HTTP_SERVER_NAME}" String
}
if (extraAttributes.contains(SemanticAttributes.HTTP_TARGET)) {
"${SemanticAttributes.HTTP_TARGET}" endpoint.path + "?id=$requestId"
}
if (extraAttributes.contains(SemanticAttributes.NET_PEER_NAME)) {
"${SemanticAttributes.NET_PEER_NAME}" "localhost"
}
if (extraAttributes.contains(SemanticAttributes.NET_TRANSPORT)) {
"${SemanticAttributes.NET_TRANSPORT}" IP_TCP
}
} }
} }
} }