import datadog.trace.agent.test.AgentTestRunner import datadog.trace.agent.test.utils.OkHttpUtils import datadog.trace.api.DDSpanTypes import datadog.trace.bootstrap.instrumentation.api.Tags import dd.trace.instrumentation.springwebflux.server.EchoHandlerFunction import dd.trace.instrumentation.springwebflux.server.FooModel import dd.trace.instrumentation.springwebflux.server.SpringWebFluxTestApplication import dd.trace.instrumentation.springwebflux.server.TestController import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory import org.springframework.boot.web.server.LocalServerPort import org.springframework.context.annotation.Bean import org.springframework.web.server.ResponseStatusException @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = [SpringWebFluxTestApplication, ForceNettyAutoConfiguration]) class SpringWebfluxTest extends AgentTestRunner { @TestConfiguration static class ForceNettyAutoConfiguration { @Bean NettyReactiveWebServerFactory nettyFactory() { return new NettyReactiveWebServerFactory() } } static final okhttp3.MediaType PLAIN_TYPE = okhttp3.MediaType.parse("text/plain; charset=utf-8") static final String INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX = SpringWebFluxTestApplication.getName() + "\$" static final String SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX = SpringWebFluxTestApplication.getSimpleName() + "\$" @LocalServerPort private int port OkHttpClient client = OkHttpUtils.client(true) def "Basic GET test #testName"() { setup: String url = "http://localhost:$port$urlPath" def request = new Request.Builder().url(url).get().build() when: def response = client.newCall(request).execute() then: response.code == 200 response.body().string() == expectedResponseBody assertTraces(1) { trace(0, 2) { span(0) { if (annotatedMethod == null) { // Functional API resourceNameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle") operationNameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle") } else { // Annotation API resourceName TestController.getSimpleName() + "." + annotatedMethod operationName TestController.getSimpleName() + "." + annotatedMethod } spanType DDSpanTypes.HTTP_SERVER childOf(span(1)) tags { "$Tags.COMPONENT" "spring-webflux-controller" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER if (annotatedMethod == null) { // Functional API "request.predicate" "(GET && $urlPathWithVariables)" "handler.type" { String tagVal -> return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX) } } else { // Annotation API "handler.type" TestController.getName() } defaultTags() } } span(1) { resourceName "GET $urlPathWithVariables" operationName "netty.request" spanType DDSpanTypes.HTTP_SERVER parent() tags { "$Tags.COMPONENT" "netty" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer "$Tags.HTTP_URL" url "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_STATUS" 200 defaultTags() } } } } where: testName | urlPath | urlPathWithVariables | annotatedMethod | expectedResponseBody "functional API without parameters" | "/greet" | "/greet" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE "functional API with one parameter" | "/greet/WORLD" | "/greet/{name}" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " WORLD" "functional API with two parameters" | "/greet/World/Test1" | "/greet/{name}/{word}" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " World Test1" "functional API delayed response" | "/greet-delayed" | "/greet-delayed" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE "annotation API without parameters" | "/foo" | "/foo" | "getFooModel" | new FooModel(0L, "DEFAULT").toString() "annotation API with one parameter" | "/foo/1" | "/foo/{id}" | "getFooModel" | new FooModel(1L, "pass").toString() "annotation API with two parameters" | "/foo/2/world" | "/foo/{id}/{name}" | "getFooModel" | new FooModel(2L, "world").toString() "annotation API delayed response" | "/foo-delayed" | "/foo-delayed" | "getFooDelayed" | new FooModel(3L, "delayed").toString() } def "GET test with async response #testName"() { setup: String url = "http://localhost:$port$urlPath" def request = new Request.Builder().url(url).get().build() when: def response = client.newCall(request).execute() then: response.code == 200 response.body().string() == expectedResponseBody assertTraces(1) { println TEST_WRITER trace(0, 3) { span(0) { if (annotatedMethod == null) { // Functional API resourceNameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle") operationNameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle") } else { // Annotation API resourceName TestController.getSimpleName() + "." + annotatedMethod operationName TestController.getSimpleName() + "." + annotatedMethod } spanType DDSpanTypes.HTTP_SERVER childOf(span(1)) tags { "$Tags.COMPONENT" "spring-webflux-controller" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER if (annotatedMethod == null) { // Functional API "request.predicate" "(GET && $urlPathWithVariables)" "handler.type" { String tagVal -> return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX) } } else { // Annotation API "handler.type" TestController.getName() } defaultTags() } } span(1) { resourceName "GET $urlPathWithVariables" operationName "netty.request" spanType DDSpanTypes.HTTP_SERVER parent() tags { "$Tags.COMPONENT" "netty" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer "$Tags.HTTP_URL" url "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_STATUS" 200 defaultTags() } } span(2) { serviceName "unnamed-java-app" if (annotatedMethod == null) { // Functional API resourceName "SpringWebFluxTestApplication.tracedMethod" operationName "trace.annotation" } else { // Annotation API resourceName "TestController.tracedMethod" operationName "trace.annotation" } childOf(span(0)) errored false tags { "$Tags.COMPONENT" "trace" defaultTags() } } } } where: testName | urlPath | urlPathWithVariables | annotatedMethod | expectedResponseBody "functional API traced method from mono" | "/greet-mono-from-callable/4" | "/greet-mono-from-callable/{id}" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " 4" "functional API traced method" | "/greet-traced-method/5" | "/greet-traced-method/{id}" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " 5" "functional API traced method with delay" | "/greet-delayed-mono/6" | "/greet-delayed-mono/{id}" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " 6" "annotation API traced method from mono" | "/foo-mono-from-callable/7" | "/foo-mono-from-callable/{id}" | "getMonoFromCallable" | new FooModel(7L, "tracedMethod").toString() "annotation API traced method" | "/foo-traced-method/8" | "/foo-traced-method/{id}" | "getTracedMethod" | new FooModel(8L, "tracedMethod").toString() "annotation API traced method with delay" | "/foo-delayed-mono/9" | "/foo-delayed-mono/{id}" | "getFooDelayedMono" | new FooModel(9L, "tracedMethod").toString() } def "404 GET test"() { setup: String url = "http://localhost:$port/notfoundgreet" def request = new Request.Builder().url(url).get().build() when: def response = client.newCall(request).execute() then: response.code == 404 assertTraces(1) { trace(0, 2) { span(0) { resourceName "404" operationName "netty.request" spanType DDSpanTypes.HTTP_SERVER parent() tags { "$Tags.COMPONENT" "netty" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer "$Tags.HTTP_URL" url "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_STATUS" 404 defaultTags() } } span(1) { resourceName "ResourceWebHandler.handle" operationName "ResourceWebHandler.handle" spanType DDSpanTypes.HTTP_SERVER childOf(span(0)) errored true tags { "$Tags.COMPONENT" "spring-webflux-controller" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "handler.type" "org.springframework.web.reactive.resource.ResourceWebHandler" errorTags(ResponseStatusException, String) defaultTags() } } } } } def "Basic POST test"() { setup: String echoString = "TEST" String url = "http://localhost:$port/echo" RequestBody body = RequestBody.create(PLAIN_TYPE, echoString) def request = new Request.Builder().url(url).post(body).build() when: def response = client.newCall(request).execute() then: response.code() == 202 response.body().string() == echoString assertTraces(1) { trace(0, 3) { span(0) { resourceName EchoHandlerFunction.getSimpleName() + ".handle" operationName EchoHandlerFunction.getSimpleName() + ".handle" spanType DDSpanTypes.HTTP_SERVER childOf(span(1)) tags { "$Tags.COMPONENT" "spring-webflux-controller" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "request.predicate" "(POST && /echo)" "handler.type" { String tagVal -> return tagVal.contains(EchoHandlerFunction.getName()) } defaultTags() } } span(1) { resourceName "POST /echo" operationName "netty.request" spanType DDSpanTypes.HTTP_SERVER parent() tags { "$Tags.COMPONENT" "netty" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer "$Tags.HTTP_URL" url "$Tags.HTTP_METHOD" "POST" "$Tags.HTTP_STATUS" 202 defaultTags() } } span(2) { resourceName "echo" operationName "echo" childOf(span(0)) tags { "$Tags.COMPONENT" "trace" defaultTags() } } } } } def "GET to bad endpoint #testName"() { setup: String url = "http://localhost:$port$urlPath" def request = new Request.Builder().url(url).get().build() when: def response = client.newCall(request).execute() then: response.code() == 500 assertTraces(1) { trace(0, 2) { span(0) { resourceName "GET $urlPathWithVariables" operationName "netty.request" spanType DDSpanTypes.HTTP_SERVER errored true parent() tags { "$Tags.COMPONENT" "netty" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer "$Tags.HTTP_URL" url "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_STATUS" 500 "error" true defaultTags() } } span(1) { if (annotatedMethod == null) { // Functional API resourceNameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle") operationNameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle") } else { // Annotation API resourceName TestController.getSimpleName() + "." + annotatedMethod operationName TestController.getSimpleName() + "." + annotatedMethod } spanType DDSpanTypes.HTTP_SERVER childOf(span(0)) errored true tags { "$Tags.COMPONENT" "spring-webflux-controller" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER if (annotatedMethod == null) { // Functional API "request.predicate" "(GET && $urlPathWithVariables)" "handler.type" { String tagVal -> return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX) } } else { // Annotation API "handler.type" TestController.getName() } errorTags(RuntimeException, "bad things happen") defaultTags() } } } } where: testName | urlPath | urlPathWithVariables | annotatedMethod "functional API fail fast" | "/greet-failfast/1" | "/greet-failfast/{id}" | null "functional API fail Mono" | "/greet-failmono/1" | "/greet-failmono/{id}" | null "annotation API fail fast" | "/foo-failfast/1" | "/foo-failfast/{id}" | "getFooFailFast" "annotation API fail Mono" | "/foo-failmono/1" | "/foo-failmono/{id}" | "getFooFailMono" } def "Redirect test"() { setup: String url = "http://localhost:$port/double-greet-redirect" String finalUrl = "http://localhost:$port/double-greet" def request = new Request.Builder().url(url).get().build() when: def response = client.newCall(request).execute() then: response.code == 200 assertTraces(2) { // TODO: why order of spans is different in these traces? trace(0, 2) { span(0) { resourceName "GET /double-greet-redirect" operationName "netty.request" spanType DDSpanTypes.HTTP_SERVER parent() tags { "$Tags.COMPONENT" "netty" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer "$Tags.HTTP_URL" url "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_STATUS" 307 defaultTags() } } span(1) { resourceName "RedirectComponent.lambda" operationName "RedirectComponent.lambda" spanType DDSpanTypes.HTTP_SERVER childOf(span(0)) tags { "$Tags.COMPONENT" "spring-webflux-controller" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "request.predicate" "(GET && /double-greet-redirect)" "handler.type" { String tagVal -> return (tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX) || tagVal.contains("Lambda")) } defaultTags() } } } trace(1, 2) { span(0) { resourceNameContains(SpringWebFluxTestApplication.getSimpleName() + "\$", ".handle") operationNameContains(SpringWebFluxTestApplication.getSimpleName() + "\$", ".handle") spanType DDSpanTypes.HTTP_SERVER childOf(span(1)) tags { "$Tags.COMPONENT" "spring-webflux-controller" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "request.predicate" "(GET && /double-greet)" "handler.type" { String tagVal -> return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX) } defaultTags() } } span(1) { resourceName "GET /double-greet" operationName "netty.request" spanType DDSpanTypes.HTTP_SERVER parent() tags { "$Tags.COMPONENT" "netty" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer "$Tags.HTTP_URL" finalUrl "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_STATUS" 200 defaultTags() } } } } } def "Multiple GETs to delaying route #testName"() { setup: def requestsCount = 50 // Should be more than 2x CPUs to fish out some bugs String url = "http://localhost:$port$urlPath" def request = new Request.Builder().url(url).get().build() when: def responses = (0..requestsCount - 1).collect { client.newCall(request).execute() } then: responses.every { it.code == 200 } responses.every { it.body().string() == expectedResponseBody } assertTraces(responses.size()) { responses.eachWithIndex { def response, int i -> trace(i, 2) { span(0) { if (annotatedMethod == null) { // Functional API resourceNameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle") operationNameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle") } else { // Annotation API resourceName TestController.getSimpleName() + "." + annotatedMethod operationName TestController.getSimpleName() + "." + annotatedMethod } spanType DDSpanTypes.HTTP_SERVER childOf(span(1)) tags { "$Tags.COMPONENT" "spring-webflux-controller" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER if (annotatedMethod == null) { // Functional API "request.predicate" "(GET && $urlPathWithVariables)" "handler.type" { String tagVal -> return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX) } } else { // Annotation API "handler.type" TestController.getName() } defaultTags() } } span(1) { resourceName "GET $urlPathWithVariables" operationName "netty.request" spanType DDSpanTypes.HTTP_SERVER parent() tags { "$Tags.COMPONENT" "netty" "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER "$Tags.PEER_HOST_IPV4" "127.0.0.1" "$Tags.PEER_PORT" Integer "$Tags.HTTP_URL" url "$Tags.HTTP_METHOD" "GET" "$Tags.HTTP_STATUS" 200 defaultTags() } } } } } where: testName | urlPath | urlPathWithVariables | annotatedMethod | expectedResponseBody "functional API delayed response" | "/greet-delayed" | "/greet-delayed" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE "annotation API delayed response" | "/foo-delayed" | "/foo-delayed" | "getFooDelayed" | new FooModel(3L, "delayed").toString() } }