Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ dependencies {
testImplementation group: 'io.vertx', name: 'vertx-web', version: '3.4.0'
testImplementation group: 'io.vertx', name: 'vertx-web-client', version: '3.4.0'

testImplementation group: 'org.mockito', name: 'mockito-inline', version: '4.11.0'

testImplementation project(':dd-java-agent:appsec:appsec-test-fixtures')

testImplementation libs.junit.jupiter

latestDepTestImplementation group: 'io.vertx', name: 'vertx-web', version: '3.+'
latestDepTestImplementation group: 'io.vertx', name: 'vertx-web-client', version: '3.+'
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package datadog.trace.instrumentation.vertx_3_4.server;

import static datadog.trace.instrumentation.vertx_3_4.server.RouteHandlerWrapper.HANDLER_SPAN_CONTEXT_KEY;
import static datadog.trace.instrumentation.vertx_3_4.server.VertxDecorator.DECORATE;

import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;

Expand All @@ -18,16 +14,12 @@ public class EndHandlerWrapper implements Handler<Void> {

@Override
public void handle(final Void event) {
AgentSpan span = routingContext.get(HANDLER_SPAN_CONTEXT_KEY);
try {
if (actual != null) {
actual.handle(event);
}
} finally {
if (span != null) {
DECORATE.onResponse(span, routingContext.response());
span.finish();
}
RouteHandlerWrapper.finishHandlerSpan(routingContext);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,16 @@ public void handle(final RoutingContext routingContext) {
span = startSpan("vertx", INSTRUMENTATION_NAME);
routingContext.put(HANDLER_SPAN_CONTEXT_KEY, span);

// Register three hooks that fire on response outcome:
// finishHandlerSpan is idempotent; whichever hook fires first wins.
//
// Known remaining gap: sendFile() failures on file-not-found or
// IOException-during-open log via HttpServerResponseImpl and return
// without firing any of the three hooks above (when the caller did
// not pass a resultHandler), so the span still leaks on that path.
routingContext.response().endHandler(new EndHandlerWrapper(routingContext));
routingContext.addBodyEndHandler(v -> finishHandlerSpan(routingContext));
Comment thread
rithikanarayan marked this conversation as resolved.
routingContext.response().exceptionHandler(t -> finishHandlerSpan(routingContext));
DECORATE.afterStart(span);
span.setResourceName(DECORATE.className(actual.getClass()));
}
Expand All @@ -63,6 +72,20 @@ public void handle(final RoutingContext routingContext) {
}
}

// Idempotently finish the route-handler span. Any of the three registered
// hooks (EndHandlerWrapper, the addBodyEndHandler fallback, or the
// response.exceptionHandler lambda) may call this; the first one to win
// clears HANDLER_SPAN_CONTEXT_KEY so the others are no-ops.
static void finishHandlerSpan(final RoutingContext routingContext) {
final AgentSpan span = routingContext.get(HANDLER_SPAN_CONTEXT_KEY);
if (span == null) {
return;
}
routingContext.put(HANDLER_SPAN_CONTEXT_KEY, null);
DECORATE.onResponse(span, routingContext.response());
span.finish();
}

private void setRoute(RoutingContext routingContext) {
final AgentSpan parentSpan = routingContext.get(PARENT_SPAN_CONTEXT_KEY);
if (parentSpan == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.vertx.core.http.impl;

import io.vertx.core.http.HttpServerResponse;

/**
* Test-side bridge that fires the package-private HttpServerResponseImpl.handleException on a
* Vert.x 3.x server response. Used by server.RouteHandlerExceptionHandlerTest to deterministically
* reproduce the non-CLOSED_EXCEPTION I/O-failure path that Vert.x exposes via
* response.exceptionHandler(...).
*/
public final class ResponseExceptionFiringHelper {
private ResponseExceptionFiringHelper() {}

public static void fireException(HttpServerResponse response, Throwable cause) {
((HttpServerResponseImpl) response).handleException(cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package server;

import static datadog.trace.agent.test.assertions.SpanMatcher.span;
import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME;
import static datadog.trace.agent.test.assertions.TraceMatcher.trace;

import datadog.trace.agent.test.AbstractInstrumentationTest;
import datadog.trace.api.DDSpanTypes;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.impl.ResponseExceptionFiringHelper;
import io.vertx.ext.web.Router;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.ServerSocket;
import java.net.URL;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

/**
* Regression test for the vertx-web 3.x route-handler span lifecycle on the
* response.exceptionHandler path.
*
* <p>HttpServerResponseImpl.handleException is invoked by Vert.x on non-CLOSED_EXCEPTION I/O
* failures of the response. Neither endHandler nor bodyEndHandler fires on this path, so the
* route-handler span would leak without an exception handler registered. The route handler here
* fires handleException directly via ResponseExceptionFiringHelper (the package-private method
* Vert.x itself uses internally), then calls response.end() normally so the HTTP client gets a
* response.
*/
class RouteHandlerExceptionHandlerTest extends AbstractInstrumentationTest {

private static Vertx vertx;
private static HttpServer server;
private static int port;

@BeforeAll
static void startServer() throws Exception {
try (ServerSocket socket = new ServerSocket(0)) {
port = socket.getLocalPort();
}

vertx = Vertx.vertx();
Router router = Router.router(vertx);
router
.route("/fail")
.handler(
ctx -> {
ResponseExceptionFiringHelper.fireException(
ctx.response(), new IOException("simulated response I/O failure"));
try {
ctx.response().setStatusCode(500).end("error");
} catch (IllegalStateException ignore) {
// handleException may have left the response in a state where end() is rejected;
// the span is already finished by our registered exception handler.
}
});

CountDownLatch ready = new CountDownLatch(1);
server =
vertx
.createHttpServer()
.requestHandler(router::accept)
.listen(
port,
result -> {
if (result.failed()) {
throw new RuntimeException("Failed to start Vert.x server", result.cause());
}
ready.countDown();
});
if (!ready.await(10, TimeUnit.SECONDS)) {
throw new IllegalStateException("Vert.x server did not start in time");
}
}

@AfterAll
static void stopServer() throws Exception {
if (server != null) {
CountDownLatch closed = new CountDownLatch(1);
server.close(ar -> closed.countDown());
closed.await(10, TimeUnit.SECONDS);
}
if (vertx != null) {
CountDownLatch closed = new CountDownLatch(1);
vertx.close(ar -> closed.countDown());
closed.await(10, TimeUnit.SECONDS);
}
}

@Test
void exceptionHandlerFinishesRouteHandlerSpan() throws Exception {
HttpURLConnection conn =
(HttpURLConnection) new URL("http://localhost:" + port + "/fail").openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
try {
// We don't care about the response code or body — only that the trace flushes.
conn.getResponseCode();
} catch (IOException ignore) {
// If end() was rejected after handleException, the client may see a closed connection.
} finally {
conn.disconnect();
}

// The netty.request span is marked as errored because the route handler ends with
// HTTP 500; the route-handler span is finished by our exception handler before
// setStatusCode(500), so it sees status=200 (default) and is not errored.
assertTraces(
trace(
SORT_BY_START_TIME,
span()
.operationName(Pattern.compile(Pattern.quote("netty.request")))
.type(DDSpanTypes.HTTP_SERVER)
.error(),
span()
.childOfPrevious()
.operationName(Pattern.compile(Pattern.quote("vertx.route-handler")))
.type(DDSpanTypes.HTTP_SERVER)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package server;

import static datadog.trace.agent.test.assertions.SpanMatcher.span;
import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME;
import static datadog.trace.agent.test.assertions.TraceMatcher.trace;
import static org.junit.jupiter.api.Assertions.assertEquals;
Comment thread
rithikanarayan marked this conversation as resolved.

import datadog.trace.agent.test.AbstractInstrumentationTest;
import datadog.trace.api.DDSpanTypes;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.ext.web.Router;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.ServerSocket;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

/**
* Regression test for the vertx-web 3.x route-handler span lifecycle on the response.sendFile(...)
* path.
*
* <p>HttpServerResponseImpl.doSendFile (vertx-core 3.x) only invokes bodyEndHandler after the file
* is written; it never invokes endHandler. With only the endHandler registration (pre-fix), the
* vertx.route-handler span never finishes on this path, the trace fails to flush, and assertTraces
* times out. With the fallback addBodyEndHandler registration, the span finishes on every
* response-end path.
*/
class RouteHandlerSendFileTest extends AbstractInstrumentationTest {

private static Vertx vertx;
private static HttpServer server;
private static int port;
private static Path payload;

@BeforeAll
static void startServer() throws Exception {
payload = Files.createTempFile("vertx-sendfile-", ".txt");
Files.write(payload, "vertx sendFile payload\n".getBytes(StandardCharsets.UTF_8));
payload.toFile().deleteOnExit();

try (ServerSocket socket = new ServerSocket(0)) {
port = socket.getLocalPort();
}

vertx = Vertx.vertx();
Router router = Router.router(vertx);
router
.route("/sendfile")
.handler(ctx -> ctx.response().sendFile(payload.toAbsolutePath().toString()));

CountDownLatch ready = new CountDownLatch(1);
server =
vertx
.createHttpServer()
.requestHandler(router::accept)
.listen(
port,
result -> {
if (result.failed()) {
throw new RuntimeException("Failed to start Vert.x server", result.cause());
}
ready.countDown();
});
if (!ready.await(10, TimeUnit.SECONDS)) {
throw new IllegalStateException("Vert.x server did not start in time");
}
}

@AfterAll
static void stopServer() throws Exception {
if (server != null) {
CountDownLatch closed = new CountDownLatch(1);
server.close(ar -> closed.countDown());
closed.await(10, TimeUnit.SECONDS);
}
if (vertx != null) {
CountDownLatch closed = new CountDownLatch(1);
vertx.close(ar -> closed.countDown());
closed.await(10, TimeUnit.SECONDS);
}
if (payload != null) {
Files.deleteIfExists(payload);
}
}

@Test
void sendFileFinishesRouteHandlerSpan() throws Exception {
HttpURLConnection conn =
(HttpURLConnection) new URL("http://localhost:" + port + "/sendfile").openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
assertEquals(200, conn.getResponseCode());
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
assertEquals("vertx sendFile payload", reader.readLine());
}

// Pre-fix: the route-handler span never finishes on the sendFile path, so the trace
// is never published and assertTraces times out waiting for the trace to flush.
assertTraces(
trace(
SORT_BY_START_TIME,
span()
.operationName(Pattern.compile(Pattern.quote("netty.request")))
.type(DDSpanTypes.HTTP_SERVER),
span()
.childOfPrevious()
.operationName(Pattern.compile(Pattern.quote("vertx.route-handler")))
.type(DDSpanTypes.HTTP_SERVER)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ dependencies {

testImplementation project(':dd-java-agent:appsec:appsec-test-fixtures')

testImplementation libs.junit.jupiter

testRuntimeOnly project(':dd-java-agent:instrumentation:jackson-core:jackson-core-common')
testRuntimeOnly project(':dd-java-agent:instrumentation:netty:netty-buffer-4.0')

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package datadog.trace.instrumentation.vertx_4_0.server;

import static datadog.trace.instrumentation.vertx_4_0.server.RouteHandlerWrapper.HANDLER_SPAN_CONTEXT_KEY;
import static datadog.trace.instrumentation.vertx_4_0.server.VertxDecorator.DECORATE;

import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;

Expand All @@ -18,16 +14,12 @@ public class EndHandlerWrapper implements Handler<Void> {

@Override
public void handle(final Void event) {
AgentSpan span = routingContext.get(HANDLER_SPAN_CONTEXT_KEY);
try {
if (actual != null) {
actual.handle(event);
}
} finally {
if (span != null) {
DECORATE.onResponse(span, routingContext.response());
span.finish();
}
RouteHandlerWrapper.finishHandlerSpan(routingContext);
}
}
}
Loading