528 lines
20 KiB
Groovy
528 lines
20 KiB
Groovy
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()
|
|
}
|
|
}
|