Test captured HTTP headers - HTTP server tests, part 1 (#4320)

* Test captured HTTP headers - HTTP server tests, part 1

* Upgrade undertow in resteasy tests (Undertow 1.0 had a bug where it thrown NPE on getHeaders())
This commit is contained in:
Mateusz Rzeszutek 2021-10-07 22:41:48 +02:00 committed by GitHub
parent fd872b0d8a
commit 581a5e3980
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 126 additions and 6 deletions

View File

@ -40,7 +40,7 @@ dependencies {
testLibrary("org.jboss.resteasy:resteasy-undertow:3.0.4.Final") {
exclude("org.jboss.resteasy", "resteasy-client")
}
testLibrary("io.undertow:undertow-servlet:1.0.0.Final")
testLibrary("io.undertow:undertow-servlet:1.4.28.Final")
testLibrary("org.jboss.resteasy:resteasy-servlet-initializer:3.0.4.Final")
latestDepTestLibrary("org.jboss.resteasy:resteasy-jaxrs:3.+")

View File

@ -14,6 +14,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.restlet.data.Form;
import org.restlet.data.Parameter;
import org.restlet.data.Reference;
import org.restlet.data.Request;
@ -51,7 +52,11 @@ final class RestletHttpAttributesExtractor
@Override
protected List<String> requestHeader(Request request, String name) {
return parametersToList(getHeaders(request).subList(name, /* ignoreCase = */ true));
Form headers = getHeaders(request);
if (headers == null) {
return Collections.emptyList();
}
return parametersToList(headers.subList(name, /* ignoreCase = */ true));
}
@Override
@ -103,7 +108,11 @@ final class RestletHttpAttributesExtractor
@Override
protected List<String> responseHeader(Request request, Response response, String name) {
return parametersToList(getHeaders(response).subList(name, /* ignoreCase = */ true));
Form headers = getHeaders(response);
if (headers == null) {
return Collections.emptyList();
}
return parametersToList(headers.subList(name, /* ignoreCase = */ true));
}
// minimize memory overhead by not using streams

View File

@ -15,8 +15,9 @@ class RestletServerTest extends AbstractRestletServerTest implements LibraryTest
@Override
Restlet wrapRestlet(Restlet restlet, String path){
RestletTracing tracing = RestletTracing.create(openTelemetry)
RestletTracing tracing = RestletTracing.newBuilder(openTelemetry)
.captureHttpHeaders(capturedHttpHeadersForTesting())
.build()
def tracingFilter = tracing.newFilter(path)
def statusFilter = new StatusFilter(component.getContext(), false, null, null)

View File

@ -15,6 +15,7 @@ import org.restlet.Restlet
import org.restlet.Router
import org.restlet.Server
import org.restlet.VirtualHost
import org.restlet.data.Form
import org.restlet.data.MediaType
import org.restlet.data.Protocol
import org.restlet.data.Request
@ -22,6 +23,7 @@ import org.restlet.data.Response
import org.restlet.data.Status
import org.restlet.util.Template
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.CAPTURE_HEADERS
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
@ -121,6 +123,20 @@ abstract class AbstractRestletServerTest extends HttpServerTest<Server> {
}
})
attachAndWrap("/captureHeaders", new Restlet() {
@Override
void handle(Request request, Response response) {
controller(CAPTURE_HEADERS) {
Form requestHeaders = request.getAttributes().get("org.restlet.http.headers")
Form responseHeaders = response.getAttributes().computeIfAbsent("org.restlet.http.headers", { new Form() })
responseHeaders.add("X-Test-Response", requestHeaders.getValues("X-Test-Request"))
response.setEntity(CAPTURE_HEADERS.getBody(), MediaType.TEXT_PLAIN)
response.setStatus(Status.valueOf(CAPTURE_HEADERS.getStatus()), CAPTURE_HEADERS.getBody())
}
}
})
attachAndWrap(INDEXED_CHILD.path, new Restlet() {
@Override
void handle(Request request, Response response) {
@ -150,6 +166,11 @@ abstract class AbstractRestletServerTest extends HttpServerTest<Server> {
true
}
@Override
boolean testCapturedHttpHeaders() {
true
}
@Override
String expectedServerSpanName(ServerEndpoint endpoint) {
switch (endpoint) {

View File

@ -71,6 +71,11 @@ class SpringBootBasedTest extends HttpServerTest<ConfigurableApplicationContext>
true
}
@Override
boolean testCapturedHttpHeaders() {
true
}
@Override
boolean hasErrorPageSpans(ServerEndpoint endpoint) {
endpoint == NOT_FOUND

View File

@ -11,11 +11,13 @@ import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseBody
import org.springframework.web.servlet.view.RedirectView
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.CAPTURE_HEADERS
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.PATH_PARAM
@ -72,6 +74,15 @@ class TestController {
}
}
@RequestMapping("/captureHeaders")
ResponseEntity capture_headers(@RequestHeader("X-Test-Request") String testRequestHeader) {
HttpServerTest.controller(CAPTURE_HEADERS) {
ResponseEntity.ok()
.header("X-Test-Response", testRequestHeader)
.body(CAPTURE_HEADERS.body)
}
}
@RequestMapping("/path/{id}/param")
@ResponseBody
String path_param(@PathVariable("id") int id) {

View File

@ -25,6 +25,7 @@ dependencies {
api("io.opentelemetry:opentelemetry-sdk-testing")
api("io.opentelemetry:opentelemetry-sdk-metrics")
api("io.opentelemetry:opentelemetry-sdk-metrics-testing")
api(project(":instrumentation-api"))
api("org.assertj:assertj-core")
@ -48,7 +49,6 @@ dependencies {
implementation("org.slf4j:jul-to-slf4j")
implementation("io.opentelemetry:opentelemetry-extension-annotations")
implementation("io.opentelemetry:opentelemetry-exporter-logging")
implementation(project(":instrumentation-api"))
annotationProcessor("com.google.auto.service:auto-service")
compileOnly("com.google.auto.service:auto-service")

View File

@ -11,6 +11,7 @@ import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.api.trace.StatusCode
import io.opentelemetry.context.Context
import io.opentelemetry.instrumentation.api.instrumenter.http.CapturedHttpHeaders
import io.opentelemetry.instrumentation.test.InstrumentationSpecification
import io.opentelemetry.instrumentation.test.asserts.TraceAssert
import io.opentelemetry.sdk.trace.data.SpanData
@ -25,6 +26,7 @@ import spock.lang.Unroll
import java.util.concurrent.Callable
import java.util.concurrent.CountDownLatch
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.CAPTURE_HEADERS
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
@ -35,11 +37,21 @@ import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEn
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS
import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP
import static java.util.Collections.singletonList
import static org.junit.Assume.assumeTrue
@Unroll
abstract class HttpServerTest<SERVER> extends InstrumentationSpecification implements HttpServerTestTrait<SERVER> {
static final String TEST_REQUEST_HEADER = "X-Test-Request"
static final String TEST_RESPONSE_HEADER = "X-Test-Response"
static CapturedHttpHeaders capturedHttpHeadersForTesting() {
CapturedHttpHeaders.create(
singletonList(TEST_REQUEST_HEADER),
singletonList(TEST_RESPONSE_HEADER))
}
String expectedServerSpanName(ServerEndpoint endpoint) {
switch (endpoint) {
case PATH_PARAM:
@ -91,6 +103,11 @@ abstract class HttpServerTest<SERVER> extends InstrumentationSpecification imple
false
}
// TODO: enable it everywhere
boolean testCapturedHttpHeaders() {
false
}
boolean testErrorBody() {
true
}
@ -125,6 +142,7 @@ abstract class HttpServerTest<SERVER> extends InstrumentationSpecification imple
ERROR("error-status", 500, "controller error"), // "error" is a special path for some frameworks
EXCEPTION("exception", 500, "controller exception"),
NOT_FOUND("notFound", 404, "not found"),
CAPTURE_HEADERS("captureHeaders", 200, "headers captured"),
// TODO: add tests for the following cases:
QUERY_PARAM("query?some=query", 200, "some=query"),
@ -369,6 +387,24 @@ abstract class HttpServerTest<SERVER> extends InstrumentationSpecification imple
method = "GET"
}
def "test captured HTTP headers"() {
setup:
assumeTrue(testCapturedHttpHeaders())
def request = AggregatedHttpRequest.of(request(CAPTURE_HEADERS, "GET").headers()
.toBuilder()
.add(TEST_REQUEST_HEADER, "test")
.build())
def response = client.execute(request).aggregate().join()
expect:
response.status().code() == CAPTURE_HEADERS.status
response.contentUtf8() == CAPTURE_HEADERS.body
and:
assertTheTraces(1, null, null, "GET", CAPTURE_HEADERS, null, response)
}
/*
This test fires a bunch of parallel request to the fixed backend endpoint.
That endpoint is supposed to create a new child span in the context of the SERVER span.
@ -604,6 +640,10 @@ abstract class HttpServerTest<SERVER> extends InstrumentationSpecification imple
if (extraAttributes.contains(SemanticAttributes.NET_TRANSPORT)) {
"${SemanticAttributes.NET_TRANSPORT}" IP_TCP
}
if (endpoint == ServerEndpoint.CAPTURE_HEADERS) {
"http.request.header.x_test_request" { it == ["test"] }
"http.response.header.x_test_response" { it == ["test"] }
}
}
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.testing.http;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.config.ConfigPropertySource;
import java.util.HashMap;
import java.util.Map;
@AutoService(ConfigPropertySource.class)
public class CapturedHttpHeadersTestConfigSource implements ConfigPropertySource {
@Override
public Map<String, String> getProperties() {
Map<String, String> testConfig = new HashMap<>();
testConfig.put(
"otel.instrumentation.common.experimental.capture-http-headers.client.request",
"X-Test-Request");
testConfig.put(
"otel.instrumentation.common.experimental.capture-http-headers.client.response",
"X-Test-Response");
testConfig.put(
"otel.instrumentation.common.experimental.capture-http-headers.server.request",
"X-Test-Request");
testConfig.put(
"otel.instrumentation.common.experimental.capture-http-headers.server.response",
"X-Test-Response");
return testConfig;
}
}