package io.opentracing.contrib.vertx.ext.web; import io.opentracing.Scope; import io.opentracing.Span; import io.opentracing.SpanContext; import io.opentracing.contrib.vertx.ext.web.WebSpanDecorator.StandardTags; import io.opentracing.mock.MockSpan; import io.opentracing.mock.MockTracer; import io.opentracing.tag.Tags; import io.opentracing.util.ThreadLocalScopeManager; import io.vertx.core.VertxOptions; import io.vertx.core.http.HttpClientRequest; import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.WebTestBase; import io.vertx.ext.web.handler.TimeoutHandler; import org.awaitility.Awaitility; import org.hamcrest.core.IsEqual; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; /** * @author Pavol Loffay */ public class TracingHandlerTest extends WebTestBase { protected MockTracer mockTracer = new MockTracer(new ThreadLocalScopeManager(), MockTracer.Propagator.TEXT_MAP); @Override public void setUp() throws Exception { super.setUp(); TracingHandler withStandardTags = new TracingHandler(mockTracer, Collections.singletonList(new StandardTags())); router.route() .order(-1).handler(withStandardTags) .failureHandler(withStandardTags); } @Override protected VertxOptions getOptions() { //force one event loop to make testing active-span bugs easier return new VertxOptions().setEventLoopPoolSize(1); } @Before public void beforeTest() throws Exception { mockTracer.reset(); } @Test public void testNoURLMapping() throws Exception { { request("/noUrlMapping", HttpMethod.GET, 404); Awaitility.await().until(reportedSpansSize(), IsEqual.equalTo(1)); } List<MockSpan> mockSpans = mockTracer.finishedSpans(); Assert.assertEquals(1, mockSpans.size()); MockSpan mockSpan = mockSpans.get(0); Assert.assertEquals("GET", mockSpan.operationName()); Assert.assertEquals(5, mockSpan.tags().size()); Assert.assertEquals(404, mockSpan.tags().get(Tags.HTTP_STATUS.getKey())); Assert.assertEquals("GET", mockSpan.tags().get(Tags.HTTP_METHOD.getKey())); Assert.assertEquals("http://localhost:8080/noUrlMapping", mockSpan.tags().get(Tags.HTTP_URL.getKey())); Assert.assertEquals(0, mockSpan.logEntries().size()); } @Test public void testStandardTags() throws Exception { { router.route("/hello").handler(routingContext -> { routingContext.response() .setChunked(true) .write("hello\n") .end(); }); request("/hello", HttpMethod.GET, 200); Awaitility.await().until(reportedSpansSize(), IsEqual.equalTo(1)); } List<MockSpan> mockSpans = mockTracer.finishedSpans(); Assert.assertEquals(1, mockSpans.size()); MockSpan mockSpan = mockSpans.get(0); Assert.assertEquals("GET", mockSpan.operationName()); Assert.assertEquals(5, mockSpan.tags().size()); Assert.assertEquals(Tags.SPAN_KIND_SERVER, mockSpan.tags().get(Tags.SPAN_KIND.getKey())); Assert.assertEquals("vertx", mockSpan.tags().get(Tags.COMPONENT.getKey())); Assert.assertEquals(200, mockSpan.tags().get(Tags.HTTP_STATUS.getKey())); Assert.assertEquals("GET", mockSpan.tags().get(Tags.HTTP_METHOD.getKey())); Assert.assertEquals("http://localhost:8080/hello", mockSpan.tags().get(Tags.HTTP_URL.getKey())); Assert.assertEquals(0, mockSpan.logEntries().size()); } @Test public void testReroute() throws Exception { { router.route("/route1").handler(routingContext -> { routingContext.reroute("/route2"); }); router.route("/route2").handler(routingContext -> { routingContext.response() .setStatusCode(205) .setChunked(true) .write("dsds") .end(); }); request("/route1", HttpMethod.GET, 205); Awaitility.await().until(reportedSpansSize(), IsEqual.equalTo(1)); } List<MockSpan> mockSpans = mockTracer.finishedSpans(); Assert.assertEquals(1, mockSpans.size()); MockSpan mockSpan = mockSpans.get(0); Assert.assertEquals("GET", mockSpan.operationName()); Assert.assertEquals(5, mockSpan.tags().size()); Assert.assertEquals(205, mockSpan.tags().get(Tags.HTTP_STATUS.getKey())); Assert.assertEquals("GET", mockSpan.tags().get(Tags.HTTP_METHOD.getKey())); Assert.assertEquals("http://localhost:8080/route1", mockSpan.tags().get(Tags.HTTP_URL.getKey())); Assert.assertEquals(1, mockSpan.logEntries().size()); Assert.assertEquals(3, mockSpan.logEntries().get(0).fields().size()); Assert.assertEquals("reroute", mockSpan.logEntries().get(0).fields().get("event")); Assert.assertEquals("http://localhost:8080/route2", mockSpan.logEntries().get(0).fields().get(Tags.HTTP_URL.getKey())); Assert.assertEquals("GET", mockSpan.logEntries().get(0).fields().get(Tags.HTTP_METHOD.getKey())); } @Test public void testRerouteFailures() throws Exception { { router.route("/route1").handler(routingContext -> { routingContext.reroute("/route2"); }).failureHandler(event -> { event.response().setStatusCode(400); }); router.route("/route2").handler(routingContext -> { throw new IllegalArgumentException("e"); }).failureHandler(event -> { event.response().setStatusCode(401).end(); }); request("/route1", HttpMethod.GET, 401); Awaitility.await().until(reportedSpansSize(), IsEqual.equalTo(1)); } List<MockSpan> mockSpans = mockTracer.finishedSpans(); Assert.assertEquals(1, mockSpans.size()); MockSpan mockSpan = mockSpans.get(0); Assert.assertEquals("GET", mockSpan.operationName()); Assert.assertEquals(6, mockSpan.tags().size()); Assert.assertEquals(Boolean.TRUE, mockSpan.tags().get(Tags.ERROR.getKey())); Assert.assertEquals(401, mockSpan.tags().get(Tags.HTTP_STATUS.getKey())); Assert.assertEquals("GET", mockSpan.tags().get(Tags.HTTP_METHOD.getKey())); Assert.assertEquals("http://localhost:8080/route1", mockSpan.tags().get(Tags.HTTP_URL.getKey())); Assert.assertEquals(2, mockSpan.logEntries().size()); } @Test public void testMultipleRoutes() throws Exception { { router.route("/route").handler(routingContext -> { routingContext.response() .setChunked(true) .setStatusCode(205) .write("route1"); routingContext.next(); }); router.route("/route").handler(routingContext -> { routingContext.response() .write("route2") .end(); }); request("/route", HttpMethod.GET, 205); Awaitility.await().until(reportedSpansSize(), IsEqual.equalTo(1)); } List<MockSpan> mockSpans = mockTracer.finishedSpans(); Assert.assertEquals(1, mockSpans.size()); MockSpan mockSpan = mockSpans.get(0); Assert.assertEquals("GET", mockSpan.operationName()); Assert.assertEquals(5, mockSpan.tags().size()); Assert.assertEquals(205, mockSpan.tags().get(Tags.HTTP_STATUS.getKey())); Assert.assertEquals("GET", mockSpan.tags().get(Tags.HTTP_METHOD.getKey())); Assert.assertEquals("http://localhost:8080/route", mockSpan.tags().get(Tags.HTTP_URL.getKey())); Assert.assertEquals(0, mockSpan.logEntries().size()); } @Test public void testLocalSpan() throws Exception { { router.route("/localSpan").handler(routingContext -> { SpanContext serverSpanContext = TracingHandler.serverSpanContext(routingContext); io.opentracing.Tracer.SpanBuilder spanBuilder = mockTracer.buildSpan("localSpan"); spanBuilder.asChildOf(serverSpanContext) .start() .finish(); routingContext.response() .setStatusCode(202) .end(); }); request("/localSpan", HttpMethod.GET, 202); Awaitility.await().until(reportedSpansSize(), IsEqual.equalTo(2)); } List<MockSpan> mockSpans = mockTracer.finishedSpans(); Assert.assertEquals(2, mockSpans.size()); Assert.assertEquals(mockSpans.get(0).parentId(), mockSpans.get(1).context().spanId()); Assert.assertEquals(mockSpans.get(0).context().traceId(), mockSpans.get(1).context().traceId()); } @Test public void testFailRoutingContext() throws Exception { { router.route("/fail").handler(routingContext -> { routingContext.fail(501); }); request("/fail", HttpMethod.GET, 501); Awaitility.await().until(reportedSpansSize(), IsEqual.equalTo(1)); } List<MockSpan> mockSpans = mockTracer.finishedSpans(); Assert.assertEquals(1, mockSpans.size()); MockSpan mockSpan = mockSpans.get(0); Assert.assertEquals("GET", mockSpan.operationName()); Assert.assertEquals(6, mockSpan.tags().size()); Assert.assertEquals(Boolean.TRUE, mockSpan.tags().get(Tags.ERROR.getKey())); Assert.assertEquals(501, mockSpan.tags().get(Tags.HTTP_STATUS.getKey())); Assert.assertEquals("GET", mockSpan.tags().get(Tags.HTTP_METHOD.getKey())); Assert.assertEquals("http://localhost:8080/fail", mockSpan.tags().get(Tags.HTTP_URL.getKey())); Assert.assertEquals(0, mockSpan.logEntries().size()); } @Test public void testExceptionInHandler() throws Exception { { router.route("/exception").handler(routingContext -> { throw new IllegalArgumentException("msg"); }); request("/exception", HttpMethod.GET,500); Awaitility.await().until(reportedSpansSize(), IsEqual.equalTo(1)); } List<MockSpan> mockSpans = mockTracer.finishedSpans(); Assert.assertEquals(1, mockSpans.size()); MockSpan mockSpan = mockSpans.get(0); Assert.assertEquals("GET", mockSpan.operationName()); Assert.assertEquals(6, mockSpan.tags().size()); Assert.assertEquals(Boolean.TRUE, mockSpan.tags().get(Tags.ERROR.getKey())); Assert.assertEquals(500, mockSpan.tags().get(Tags.HTTP_STATUS.getKey())); Assert.assertEquals("GET", mockSpan.tags().get(Tags.HTTP_METHOD.getKey())); Assert.assertEquals("http://localhost:8080/exception", mockSpan.tags().get(Tags.HTTP_URL.getKey())); Assert.assertEquals(1, mockSpan.logEntries().size()); Assert.assertEquals(2, mockSpan.logEntries().get(0).fields().size()); Assert.assertEquals(Tags.ERROR.getKey(), mockSpan.logEntries().get(0).fields().get("event")); Assert.assertTrue(mockSpan.logEntries().get(0).fields().get("error.object") instanceof Throwable); } @Test public void testExceptionInHandlerWithFailureHandler() throws Exception { { router.route("/exceptionWithHandler").handler(routingContext -> { throw new IllegalArgumentException("msg"); }).failureHandler(event -> { event.response() .setStatusCode(404) .end(); }); request("/exceptionWithHandler", HttpMethod.GET, 404); Awaitility.await().until(reportedSpansSize(), IsEqual.equalTo(1)); } List<MockSpan> mockSpans = mockTracer.finishedSpans(); Assert.assertEquals(1, mockSpans.size()); MockSpan mockSpan = mockSpans.get(0); Assert.assertEquals("GET", mockSpan.operationName()); Assert.assertEquals(6, mockSpan.tags().size()); Assert.assertEquals(Boolean.TRUE, mockSpan.tags().get(Tags.ERROR.getKey())); Assert.assertEquals(404, mockSpan.tags().get(Tags.HTTP_STATUS.getKey())); Assert.assertEquals("GET", mockSpan.tags().get(Tags.HTTP_METHOD.getKey())); Assert.assertEquals("http://localhost:8080/exceptionWithHandler", mockSpan.tags().get(Tags.HTTP_URL.getKey())); Assert.assertEquals(1, mockSpan.logEntries().size()); Assert.assertEquals(2, mockSpan.logEntries().get(0).fields().size()); Assert.assertEquals(Tags.ERROR.getKey(), mockSpan.logEntries().get(0).fields().get("event")); Assert.assertTrue(mockSpan.logEntries().get(0).fields().get("error.object") instanceof Throwable); } @Test public void testTimeoutHandler() throws Exception { { router.route().handler(TimeoutHandler.create(300, 501)); router.route("/timeout") .blockingHandler(routingContext -> { try { Thread.sleep(10000); routingContext.response() .setStatusCode(202) .end(); } catch (InterruptedException e) { e.printStackTrace(); routingContext.response().end(); } }); request("/timeout", HttpMethod.GET, 501); Awaitility.await().until(reportedSpansSize(), IsEqual.equalTo(1)); } List<MockSpan> mockSpans = mockTracer.finishedSpans(); Assert.assertEquals(1, mockSpans.size()); MockSpan mockSpan = mockSpans.get(0); Assert.assertEquals("GET", mockSpan.operationName()); Assert.assertEquals(6, mockSpan.tags().size()); Assert.assertEquals(Boolean.TRUE, mockSpan.tags().get(Tags.ERROR.getKey())); Assert.assertEquals(501, mockSpan.tags().get(Tags.HTTP_STATUS.getKey())); Assert.assertEquals("GET", mockSpan.tags().get(Tags.HTTP_METHOD.getKey())); Assert.assertEquals("http://localhost:8080/timeout", mockSpan.tags().get(Tags.HTTP_URL.getKey())); Assert.assertEquals(0, mockSpan.logEntries().size()); } @Test public void testBodyEndHandler() throws Exception { { router.route("/bodyEnd").handler(routingContext -> { routingContext.addBodyEndHandler(event -> { // noop }); routingContext.response().end(); }); request("/bodyEnd", HttpMethod.GET, 200); Awaitility.await().until(reportedSpansSize(), IsEqual.equalTo(1)); } List<MockSpan> mockSpans = mockTracer.finishedSpans(); Assert.assertEquals(1, mockSpans.size()); MockSpan mockSpan = mockSpans.get(0); Assert.assertEquals("GET", mockSpan.operationName()); Assert.assertEquals(5, mockSpan.tags().size()); Assert.assertEquals(200, mockSpan.tags().get(Tags.HTTP_STATUS.getKey())); Assert.assertEquals("GET", mockSpan.tags().get(Tags.HTTP_METHOD.getKey())); Assert.assertEquals("http://localhost:8080/bodyEnd", mockSpan.tags().get(Tags.HTTP_URL.getKey())); Assert.assertEquals(0, mockSpan.logEntries().size()); } /** * If someone incorrectly starts a span on an event loop, the TracingHandler was previously using it as the current * active span to be a child of. Such functionality is correct in a thread-per-request environment but not * in an event loop model. The tracinghandler now `ignoreActiveSpans` which is a better safeguard against the * problem. * */ @Test public void testIgnoringActiveSpan() throws Exception { final CountDownLatch firstLatch = new CountDownLatch(1); final CountDownLatch secondLatch = new CountDownLatch(1); router.route("/wait").handler(context -> { Span span = mockTracer.buildSpan("internal") .start(); Scope scope = mockTracer.scopeManager().activate(span); vertx().executeBlocking((result) -> { firstLatch.countDown(); try { awaitLatch(secondLatch); } catch (InterruptedException e) { result.fail(e); } result.complete(); }, result -> { scope.close(); context.response().end(); }); }); //perform two requests -- we want to block //inside the handler and make an active span. request("/wait", HttpMethod.GET, 200); awaitLatch(firstLatch); request("/wait", HttpMethod.GET, 200); //they should both me in the router now -- resume the latch secondLatch.countDown(); Awaitility.await().until(reportedSpansSize(), IsEqual.equalTo(2)); for (MockSpan span : mockTracer.finishedSpans()) { Assert.assertEquals(span.parentId(), 0); } } protected void request(String path, HttpMethod method, int statusCode) throws InterruptedException { HttpClientRequest req = client.request(method, 8080, "localhost", path, resp -> { assertEquals(statusCode, resp.statusCode()); }); req.end(); } protected Callable<Integer> reportedSpansSize() { return () -> mockTracer.finishedSpans().size(); } }