Ratpack httpclient (#4787)

* Add Ratpack HttpClient instrumentation

* Propagate trace through Ratpack HttpClient

* Add test to verify trace propagation

* Fix spotlessApply

* Use HTTP method as name for ratpack http client

* Add current Context to the execution

* Fix HttpClient tests

* Move Ratpack HttpClient tests to java package

* Remove nullaway conventions from library

* Add Context to ExecHarness executions

* Remove ContextHolder from execution

* Fix function test using other server stub

* Fix lazy other app

* Refactor ratpack client packages

* Rename getter method
This commit is contained in:
Javier Salinas 2022-01-18 21:06:13 +01:00 committed by GitHub
parent 69fc3df19a
commit 95e240c3e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 781 additions and 16 deletions

View File

@ -1,6 +1,5 @@
plugins {
id("otel.library-instrumentation")
id("otel.nullaway-conventions")
}
dependencies {

View File

@ -0,0 +1,24 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack;
import io.opentelemetry.instrumentation.ratpack.internal.ContextHolder;
import ratpack.exec.ExecInitializer;
import ratpack.exec.Execution;
final class OpenTelemetryExecInitializer implements ExecInitializer {
public static final ExecInitializer INSTANCE = new OpenTelemetryExecInitializer();
@Override
public void init(Execution execution) {
// Propagates ContextHolder to child execution because the response interceptor is triggered in
// another execution segment
execution
.maybeParent()
.flatMap(parent -> parent.maybeGet(ContextHolder.class))
.ifPresent(execution::add);
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.ratpack.internal.ContextHolder;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import ratpack.exec.Execution;
import ratpack.http.client.HttpClient;
import ratpack.http.client.HttpResponse;
import ratpack.http.client.RequestSpec;
final class OpenTelemetryHttpClient {
private final Instrumenter<RequestSpec, HttpResponse> instrumenter;
OpenTelemetryHttpClient(Instrumenter<RequestSpec, HttpResponse> instrumenter) {
this.instrumenter = instrumenter;
}
public HttpClient instrument(HttpClient httpClient) throws Exception {
return httpClient.copyWith(
httpClientSpec -> {
httpClientSpec.requestIntercept(
requestSpec -> {
Context parentOtelCtx = Context.current();
if (!instrumenter.shouldStart(parentOtelCtx, requestSpec)) {
return;
}
Context otelCtx = instrumenter.start(parentOtelCtx, requestSpec);
Span span = Span.fromContext(otelCtx);
String path = requestSpec.getUri().getPath();
span.setAttribute(SemanticAttributes.HTTP_ROUTE, path);
Execution.current().add(new ContextHolder(otelCtx, requestSpec));
});
httpClientSpec.responseIntercept(
httpResponse -> {
Execution execution = Execution.current();
ContextHolder contextHolder = execution.get(ContextHolder.class);
execution.remove(ContextHolder.class);
instrumenter.end(
contextHolder.context(), contextHolder.requestSpec(), httpResponse, null);
});
httpClientSpec.errorIntercept(
ex -> {
Execution execution = Execution.current();
ContextHolder contextHolder = execution.get(ContextHolder.class);
execution.remove(ContextHolder.class);
instrumenter.end(contextHolder.context(), contextHolder.requestSpec(), null, ex);
});
});
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack;
import io.opentelemetry.instrumentation.api.instrumenter.http.CapturedHttpHeaders;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.util.List;
import javax.annotation.Nullable;
import ratpack.http.client.HttpResponse;
import ratpack.http.client.RequestSpec;
final class RatpackHttpClientAttributesExtractor
extends HttpClientAttributesExtractor<RequestSpec, HttpResponse> {
RatpackHttpClientAttributesExtractor(CapturedHttpHeaders capturedHttpHeaders) {
super(capturedHttpHeaders);
}
@Nullable
@Override
protected String url(RequestSpec requestSpec) {
return requestSpec.getUri().toString();
}
@Nullable
@Override
protected String flavor(RequestSpec requestSpec, @Nullable HttpResponse httpResponse) {
return SemanticAttributes.HttpFlavorValues.HTTP_1_1;
}
@Nullable
@Override
protected String method(RequestSpec requestSpec) {
return requestSpec.getMethod().getName();
}
@Override
protected List<String> requestHeader(RequestSpec requestSpec, String name) {
return requestSpec.getHeaders().getAll(name);
}
@Nullable
@Override
protected Long requestContentLength(
RequestSpec requestSpec, @Nullable HttpResponse httpResponse) {
return null;
}
@Nullable
@Override
protected Long requestContentLengthUncompressed(
RequestSpec requestSpec, @Nullable HttpResponse httpResponse) {
return null;
}
@Nullable
@Override
protected Integer statusCode(RequestSpec requestSpec, HttpResponse httpResponse) {
return httpResponse.getStatusCode();
}
@Nullable
@Override
protected Long responseContentLength(RequestSpec requestSpec, HttpResponse httpResponse) {
return null;
}
@Nullable
@Override
protected Long responseContentLengthUncompressed(
RequestSpec requestSpec, HttpResponse httpResponse) {
return null;
}
@Override
protected List<String> responseHeader(
RequestSpec requestSpec, HttpResponse httpResponse, String name) {
return httpResponse.getHeaders().getAll(name);
}
}

View File

@ -7,10 +7,14 @@ package io.opentelemetry.instrumentation.ratpack;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import ratpack.exec.ExecInitializer;
import ratpack.exec.ExecInterceptor;
import ratpack.handling.HandlerDecorator;
import ratpack.http.Request;
import ratpack.http.Response;
import ratpack.http.client.HttpClient;
import ratpack.http.client.HttpResponse;
import ratpack.http.client.RequestSpec;
import ratpack.registry.RegistrySpec;
/**
@ -42,9 +46,13 @@ public final class RatpackTracing {
}
private final OpenTelemetryServerHandler serverHandler;
private final OpenTelemetryHttpClient httpClientInstrumenter;
RatpackTracing(Instrumenter<Request, Response> serverInstrumenter) {
RatpackTracing(
Instrumenter<Request, Response> serverInstrumenter,
Instrumenter<RequestSpec, HttpResponse> clientInstrumenter) {
serverHandler = new OpenTelemetryServerHandler(serverInstrumenter);
httpClientInstrumenter = new OpenTelemetryHttpClient(clientInstrumenter);
}
/** Returns instance of {@link OpenTelemetryServerHandler} to support Ratpack Registry binding. */
@ -57,9 +65,20 @@ public final class RatpackTracing {
return OpenTelemetryExecInterceptor.INSTANCE;
}
/** Returns instance of {@link ExecInitializer} to support Ratpack Registry binding. */
public ExecInitializer getOpenTelemetryExecInitializer() {
return OpenTelemetryExecInitializer.INSTANCE;
}
/** Configures the {@link RegistrySpec} with OpenTelemetry. */
public void configureServerRegistry(RegistrySpec registry) {
registry.add(HandlerDecorator.prepend(serverHandler));
registry.add(OpenTelemetryExecInterceptor.INSTANCE);
registry.add(OpenTelemetryExecInitializer.INSTANCE);
}
/** Returns instrumented instance of {@link HttpClient} with OpenTelemetry. */
public HttpClient instrumentHttpClient(HttpClient httpClient) throws Exception {
return httpClientInstrumenter.instrument(httpClient);
}
}

View File

@ -13,11 +13,14 @@ import io.opentelemetry.instrumentation.api.instrumenter.http.CapturedHttpHeader
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerMetrics;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor;
import io.opentelemetry.instrumentation.ratpack.internal.RatpackHttpNetAttributesExtractor;
import io.opentelemetry.instrumentation.ratpack.internal.RatpackNetAttributesExtractor;
import java.util.ArrayList;
import java.util.List;
import ratpack.http.Request;
import ratpack.http.Response;
import ratpack.http.client.HttpResponse;
import ratpack.http.client.RequestSpec;
/** A builder for {@link RatpackTracing}. */
public final class RatpackTracingBuilder {
@ -30,6 +33,9 @@ public final class RatpackTracingBuilder {
new ArrayList<>();
private CapturedHttpHeaders capturedHttpHeaders = CapturedHttpHeaders.server(Config.get());
private final List<AttributesExtractor<? super RequestSpec, ? super HttpResponse>>
additionalHttpClientExtractors = new ArrayList<>();
RatpackTracingBuilder(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}
@ -44,6 +50,12 @@ public final class RatpackTracingBuilder {
return this;
}
public RatpackTracingBuilder addClientAttributeExtractor(
AttributesExtractor<? super RequestSpec, ? super HttpResponse> attributesExtractor) {
additionalHttpClientExtractors.add(attributesExtractor);
return this;
}
/**
* Configure the instrumentation to capture chosen HTTP request and response headers as span
* attributes.
@ -72,6 +84,21 @@ public final class RatpackTracingBuilder {
.addRequestMetrics(HttpServerMetrics.get())
.newServerInstrumenter(RatpackGetter.INSTANCE);
return new RatpackTracing(instrumenter);
return new RatpackTracing(instrumenter, httpClientInstrumenter());
}
private Instrumenter<RequestSpec, HttpResponse> httpClientInstrumenter() {
RatpackHttpNetAttributesExtractor netAttributes = new RatpackHttpNetAttributesExtractor();
RatpackHttpClientAttributesExtractor httpAttributes =
new RatpackHttpClientAttributesExtractor(capturedHttpHeaders);
return Instrumenter.<RequestSpec, HttpResponse>builder(
openTelemetry, INSTRUMENTATION_NAME, HttpSpanNameExtractor.create(httpAttributes))
.setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributes))
.addAttributesExtractor(netAttributes)
.addAttributesExtractor(httpAttributes)
.addAttributesExtractors(additionalHttpClientExtractors)
.addRequestMetrics(HttpServerMetrics.get())
.newClientInstrumenter(RequestHeaderSetter.INSTANCE);
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack;
import io.opentelemetry.context.propagation.TextMapSetter;
import ratpack.api.Nullable;
import ratpack.http.client.RequestSpec;
enum RequestHeaderSetter implements TextMapSetter<RequestSpec> {
INSTANCE;
@Override
public void set(@Nullable RequestSpec carrier, String key, String value) {
if (carrier != null) {
carrier.getHeaders().set(key, value);
}
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack.internal;
import io.opentelemetry.context.Context;
import ratpack.http.client.RequestSpec;
public final class ContextHolder {
private final Context context;
private final RequestSpec requestSpec;
public ContextHolder(Context context, RequestSpec requestSpec) {
this.context = context;
this.requestSpec = requestSpec;
}
public Context context() {
return context;
}
public RequestSpec requestSpec() {
return requestSpec;
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack.internal;
import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import javax.annotation.Nullable;
import ratpack.http.client.HttpResponse;
import ratpack.http.client.RequestSpec;
public final class RatpackHttpNetAttributesExtractor
extends NetClientAttributesExtractor<RequestSpec, HttpResponse> {
@Override
public String transport(RequestSpec request, @Nullable HttpResponse response) {
return SemanticAttributes.NetTransportValues.IP_TCP;
}
@Override
@Nullable
public String peerName(RequestSpec request, @Nullable HttpResponse response) {
return request.getUri().getHost();
}
@Override
public Integer peerPort(RequestSpec request, @Nullable HttpResponse response) {
return request.getUri().getPort();
}
@Override
@Nullable
public String peerIp(RequestSpec request, @Nullable HttpResponse response) {
return null;
}
}

View File

@ -5,19 +5,25 @@
package io.opentelemetry.instrumentation.ratpack
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter
import io.opentelemetry.sdk.trace.export.SpanExporter
import ratpack.guice.BindingsImposition
import ratpack.impose.ForceDevelopmentImposition
import ratpack.impose.ImpositionsSpec
import ratpack.impose.UserRegistryImposition
import ratpack.registry.Registry
import ratpack.test.MainClassApplicationUnderTest
import ratpack.test.embed.EmbeddedApp
class RatpackFunctionalTest extends MainClassApplicationUnderTest {
Registry registry
@Lazy InMemorySpanExporter spanExporter = registry.get(SpanExporter) as InMemorySpanExporter
EmbeddedApp app = EmbeddedApp.of { server ->
server.handlers { chain ->
chain.get("other") { ctx -> ctx.render("hi-other") }
}
}
RatpackFunctionalTest(Class<?> mainClass) {
super(mainClass)
@ -31,5 +37,8 @@ class RatpackFunctionalTest extends MainClassApplicationUnderTest {
registry = r
registry
})
impositions.add(BindingsImposition.of {
it.bindInstance(URI, app.address.resolve("other"))
})
}
}

View File

@ -0,0 +1,235 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack.client
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.api.trace.StatusCode
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator
import io.opentelemetry.context.propagation.ContextPropagators
import io.opentelemetry.instrumentation.ratpack.RatpackTracing
import io.opentelemetry.sdk.OpenTelemetrySdk
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter
import io.opentelemetry.sdk.trace.SdkTracerProvider
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
import ratpack.exec.Promise
import ratpack.func.Action
import ratpack.guice.Guice
import ratpack.http.client.HttpClient
import ratpack.test.embed.EmbeddedApp
import spock.lang.Specification
import spock.util.concurrent.PollingConditions
import java.time.Duration
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE
class InstrumentedHttpClientTest extends Specification {
def spanExporter = InMemorySpanExporter.create()
def tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(SimpleSpanProcessor.create(spanExporter))
.build()
def openTelemetry = OpenTelemetrySdk.builder()
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
.setTracerProvider(tracerProvider).build()
RatpackTracing ratpackTracing = RatpackTracing.create(openTelemetry)
def cleanup() {
spanExporter.reset()
}
def "propagate trace with http calls"() {
expect:
def otherApp = EmbeddedApp.of { spec ->
spec.registry(
Guice.registry { bindings ->
ratpackTracing.configureServerRegistry(bindings)
}
)
spec.handlers {
it.get("bar") { ctx -> ctx.render("foo") }
}
}
def app = EmbeddedApp.of { spec ->
spec.registry(
Guice.registry { bindings ->
ratpackTracing.configureServerRegistry(bindings)
bindings.bindInstance(HttpClient, ratpackTracing.instrumentHttpClient(HttpClient.of(Action.noop())))
}
)
spec.handlers { chain ->
chain.get("foo") { ctx ->
HttpClient instrumentedHttpClient = ctx.get(HttpClient)
instrumentedHttpClient.get(new URI("${otherApp.address}bar"))
.then { ctx.render("bar") }
}
}
}
app.test { httpClient ->
"bar" == httpClient.get("foo").body.text
}
new PollingConditions().eventually {
def spanData = spanExporter.finishedSpanItems.find { it.name == "/foo" }
def spanClientData = spanExporter.finishedSpanItems.find { it.name == "HTTP GET" && it.kind == SpanKind.CLIENT }
def spanDataApi = spanExporter.finishedSpanItems.find { it.name == "/bar" && it.kind == SpanKind.SERVER }
spanData.traceId == spanClientData.traceId
spanData.traceId == spanDataApi.traceId
spanData.kind == SpanKind.SERVER
spanClientData.kind == SpanKind.CLIENT
def atts = spanClientData.attributes.asMap()
atts[HTTP_ROUTE] == "/bar"
atts[HTTP_METHOD] == "GET"
atts[HTTP_STATUS_CODE] == 200L
def attributes = spanData.attributes.asMap()
attributes[HTTP_ROUTE] == "/foo"
attributes[SemanticAttributes.HTTP_TARGET] == "/foo"
attributes[HTTP_METHOD] == "GET"
attributes[HTTP_STATUS_CODE] == 200L
def attsApi = spanDataApi.attributes.asMap()
attsApi[HTTP_ROUTE] == "/bar"
attsApi[SemanticAttributes.HTTP_TARGET] == "/bar"
attsApi[HTTP_METHOD] == "GET"
attsApi[HTTP_STATUS_CODE] == 200L
}
}
def "add spans for multiple concurrent client calls"() {
expect:
def latch = new CountDownLatch(2)
def otherApp = EmbeddedApp.of { spec ->
spec.handlers { chain ->
chain.get("foo") { ctx -> ctx.render("bar") }
chain.get("bar") { ctx -> ctx.render("foo") }
}
}
def app = EmbeddedApp.of { spec ->
spec.registry(
Guice.registry { bindings ->
ratpackTracing.configureServerRegistry(bindings)
bindings.bindInstance(HttpClient, ratpackTracing.instrumentHttpClient(HttpClient.of(Action.noop())))
}
)
spec.handlers { chain ->
chain.get("path-name") { ctx ->
ctx.render("hello")
def instrumentedHttpClient = ctx.get(HttpClient)
instrumentedHttpClient.get(new URI("${otherApp.address}foo")).then { latch.countDown() }
instrumentedHttpClient.get(new URI("${otherApp.address}bar")).then { latch.countDown() }
}
}
}
app.test { httpClient ->
"hello" == httpClient.get("path-name").body.text
latch.await(1, TimeUnit.SECONDS)
}
new PollingConditions().eventually {
spanExporter.finishedSpanItems.size() == 3
def spanData = spanExporter.finishedSpanItems.find { spanData -> spanData.name == "/path-name" }
def spanClientData1 = spanExporter.finishedSpanItems.find { s -> s.name == "HTTP GET" && s.attributes.asMap()[HTTP_ROUTE] == "/foo" }
def spanClientData2 = spanExporter.finishedSpanItems.find { s -> s.name == "HTTP GET" && s.attributes.asMap()[HTTP_ROUTE] == "/bar" }
spanData.traceId == spanClientData1.traceId
spanData.traceId == spanClientData2.traceId
spanData.kind == SpanKind.SERVER
spanClientData1.kind == SpanKind.CLIENT
def atts = spanClientData1.attributes.asMap()
atts[HTTP_ROUTE] == "/foo"
atts[HTTP_METHOD] == "GET"
atts[HTTP_STATUS_CODE] == 200L
spanClientData2.kind == SpanKind.CLIENT
def atts2 = spanClientData2.attributes.asMap()
atts2[HTTP_ROUTE] == "/bar"
atts2[HTTP_METHOD] == "GET"
atts2[HTTP_STATUS_CODE] == 200L
def attributes = spanData.attributes.asMap()
attributes[HTTP_ROUTE] == "/path-name"
attributes[SemanticAttributes.HTTP_TARGET] == "/path-name"
attributes[HTTP_METHOD] == "GET"
attributes[HTTP_STATUS_CODE] == 200L
}
}
def "handling exception errors in http client"() {
expect:
def otherApp = EmbeddedApp.of { spec ->
spec.handlers {
it.get("foo") { ctx ->
Promise.value("bar").defer(Duration.ofSeconds(1L))
.then { ctx.render("bar") }
}
}
}
def app = EmbeddedApp.of { spec ->
spec.registry(
Guice.registry { bindings ->
ratpackTracing.configureServerRegistry(bindings)
bindings.bindInstance(HttpClient, ratpackTracing.instrumentHttpClient(
HttpClient.of { s -> s.readTimeout(Duration.ofMillis(10)) })
)
}
)
spec.handlers { chain ->
chain.get("path-name") { ctx ->
def instrumentedHttpClient = ctx.get(HttpClient)
instrumentedHttpClient.get(new URI("${otherApp.address}foo"))
.onError { ctx.render("error") }
.then { ctx.render("hello") }
}
}
}
app.test { httpClient -> "error" == httpClient.get("path-name").body.text
}
new PollingConditions().eventually {
def spanData = spanExporter.finishedSpanItems.find { it.name == "/path-name" }
def spanClientData = spanExporter.finishedSpanItems.find { it.name == "HTTP GET" }
spanData.traceId == spanClientData.traceId
spanData.kind == SpanKind.SERVER
spanClientData.kind == SpanKind.CLIENT
def atts = spanClientData.attributes.asMap()
atts[HTTP_ROUTE] == "/foo"
atts[HTTP_METHOD] == "GET"
atts[HTTP_STATUS_CODE] == null
spanClientData.status.statusCode == StatusCode.ERROR
spanClientData.events.first().name == "exception"
def attributes = spanData.attributes.asMap()
attributes[HTTP_ROUTE] == "/path-name"
attributes[SemanticAttributes.HTTP_TARGET] == "/path-name"
attributes[HTTP_METHOD] == "GET"
attributes[HTTP_STATUS_CODE] == 200L
}
}
}

View File

@ -18,15 +18,21 @@ import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter
import io.opentelemetry.sdk.trace.SdkTracerProvider
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor
import io.opentelemetry.sdk.trace.export.SpanExporter
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
import ratpack.exec.ExecInitializer
import ratpack.exec.ExecInterceptor
import ratpack.guice.Guice
import ratpack.http.client.HttpClient
import ratpack.server.RatpackServer
import spock.lang.Specification
import spock.util.concurrent.PollingConditions
import javax.inject.Singleton
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_TARGET
class RatpackServerApplicationTest extends Specification {
def app = new RatpackFunctionalTest(RatpackApp)
@ -40,10 +46,35 @@ class RatpackServerApplicationTest extends Specification {
def attributes = spanData.attributes.asMap()
spanData.kind == SpanKind.SERVER
attributes[SemanticAttributes.HTTP_ROUTE] == "/foo"
attributes[SemanticAttributes.HTTP_TARGET] == "/foo"
attributes[SemanticAttributes.HTTP_METHOD] == "GET"
attributes[SemanticAttributes.HTTP_STATUS_CODE] == 200L
attributes[HTTP_ROUTE] == "/foo"
attributes[HTTP_TARGET] == "/foo"
attributes[HTTP_METHOD] == "GET"
attributes[HTTP_STATUS_CODE] == 200L
}
}
def "propagate trace to http calls"() {
expect:
app.test { httpClient -> "hi-bar" == httpClient.get("bar").body.text }
new PollingConditions().eventually {
def spanData = app.spanExporter.finishedSpanItems.find { it.name == "/bar" }
def spanDataClient = app.spanExporter.finishedSpanItems.find { it.name == "HTTP GET" }
def attributes = spanData.attributes.asMap()
spanData.traceId == spanDataClient.traceId
spanData.kind == SpanKind.SERVER
attributes[HTTP_ROUTE] == "/bar"
attributes[HTTP_TARGET] == "/bar"
attributes[HTTP_METHOD] == "GET"
attributes[HTTP_STATUS_CODE] == 200L
spanDataClient.kind == SpanKind.CLIENT
def attributesClient = spanDataClient.attributes.asMap()
attributesClient[HTTP_ROUTE] == "/other"
attributesClient[HTTP_METHOD] == "GET"
attributesClient[HTTP_STATUS_CODE] == 200L
}
}
@ -91,6 +122,18 @@ class OpenTelemetryModule extends AbstractModule {
.build()
return OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build()
}
@Singleton
@Provides
HttpClient instrumentedHttpClient(RatpackTracing ratpackTracing) {
return ratpackTracing.instrumentHttpClient(HttpClient.of {})
}
@Singleton
@Provides
ExecInitializer ratpackExecInitializer(RatpackTracing ratpackTracing) {
return ratpackTracing.getOpenTelemetryExecInitializer()
}
}
@CompileStatic
@ -99,19 +142,17 @@ class RatpackApp {
static void main(String... args) {
RatpackServer.start { server ->
server
.registry(
Guice.registry { bindings ->
bindings
.module(OpenTelemetryModule)
}
)
.registry(Guice.registry { b -> b.module(OpenTelemetryModule) })
.handlers { chain ->
chain
.get("ignore") { ctx -> ctx.render("ignored") }
.all(OpenTelemetryServerHandler)
.get("foo") { ctx -> ctx.render("hi-foo") }
.get("bar") { ctx ->
ctx.get(HttpClient).get(ctx.get(URI))
.then { ctx.render("hi-bar") }
}
}
}
}
}

View File

@ -0,0 +1,150 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack;
import com.google.common.collect.ImmutableList;
import io.netty.channel.ConnectTimeoutException;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest;
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions;
import java.net.URI;
import java.time.Duration;
import java.util.Map;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.condition.OS;
import ratpack.exec.Operation;
import ratpack.exec.Promise;
import ratpack.exec.internal.DefaultExecController;
import ratpack.func.Action;
import ratpack.http.client.HttpClient;
import ratpack.http.client.HttpClientReadTimeoutException;
import ratpack.http.client.HttpClientSpec;
import ratpack.http.client.ReceivedResponse;
import ratpack.test.exec.ExecHarness;
abstract class AbstractRatpackHttpClientTest extends AbstractHttpClientTest<Void> {
final ExecHarness exec = ExecHarness.harness();
HttpClient client;
HttpClient singleConnectionClient;
@BeforeAll
void setUpClient() throws Exception {
exec.run(
unused -> {
((DefaultExecController) exec.getController())
.setInitializers(ImmutableList.of(OpenTelemetryExecInitializer.INSTANCE));
((DefaultExecController) exec.getController())
.setInterceptors(ImmutableList.of(OpenTelemetryExecInterceptor.INSTANCE));
client = buildHttpClient();
singleConnectionClient = buildHttpClient(spec -> spec.poolSize(1));
});
}
@AfterAll
void cleanUpClient() {
client.close();
singleConnectionClient.close();
exec.close();
}
protected HttpClient buildHttpClient() throws Exception {
return buildHttpClient(Action.noop());
}
protected HttpClient buildHttpClient(Action<? super HttpClientSpec> action) throws Exception {
return HttpClient.of(action);
}
@Override
protected final Void buildRequest(String method, URI uri, Map<String, String> headers) {
return null;
}
@Override
protected final int sendRequest(Void request, String method, URI uri, Map<String, String> headers)
throws Exception {
return exec.yield(
r -> r.add(Context.class, Context.current()),
execution -> internalSendRequest(client, method, uri, headers))
.getValueOrThrow();
}
@Override
protected final void sendRequestWithCallback(
Void request,
String method,
URI uri,
Map<String, String> headers,
RequestResult requestResult)
throws Exception {
// ratpack-test 1.8 supports execute with an Action of registrySpec
exec.yield(
r -> r.add(Context.class, Context.current()),
(e) ->
Operation.of(
() ->
internalSendRequest(client, method, uri, headers)
.result(
result ->
requestResult.complete(
result::getValue, result.getThrowable())))
.promise())
.getValueOrThrow();
}
// overridden in RatpackForkedHttpClientTest
protected Promise<Integer> internalSendRequest(
HttpClient client, String method, URI uri, Map<String, String> headers) {
Promise<ReceivedResponse> resp =
client.request(
uri,
spec -> {
// Connect timeout for the whole client was added in 1.5 so we need to add timeout for
// each request
spec.connectTimeout(Duration.ofSeconds(2));
if (uri.getPath().equals("/read-timeout")) {
spec.readTimeout(readTimeout());
}
spec.method(method);
spec.headers(headersSpec -> headers.forEach(headersSpec::add));
});
return resp.map(ReceivedResponse::getStatusCode);
}
@Override
protected void configure(HttpClientTestOptions options) {
options.setSingleConnectionFactory(
(host, port) ->
(path, headers) -> {
URI uri = resolveAddress(path);
return exec.yield(
r -> r.add(Context.class, Context.current()),
unused -> internalSendRequest(singleConnectionClient, "GET", uri, headers))
.getValueOrThrow();
});
options.setClientSpanErrorMapper(
(uri, exception) -> {
if (uri.toString().equals("https://192.0.2.1/")) {
return new ConnectTimeoutException("Connect timeout (PT2S) connecting to " + uri);
} else if (OS.WINDOWS.isCurrentOs() && uri.toString().equals("http://localhost:61/")) {
return new ConnectTimeoutException("Connect timeout (PT2S) connecting to " + uri);
} else if (uri.getPath().equals("/read-timeout")) {
return new HttpClientReadTimeoutException(
"Read timeout (PT2S) waiting on HTTP server at " + uri);
}
return exception;
});
options.disableTestRedirects();
options.disableTestReusedRequest();
options.enableTestReadTimeout();
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ratpack;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension;
import org.junit.jupiter.api.extension.RegisterExtension;
import ratpack.func.Action;
import ratpack.http.client.HttpClient;
import ratpack.http.client.HttpClientSpec;
class RatpackHttpClientTest extends AbstractRatpackHttpClientTest {
@RegisterExtension
static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forLibrary();
@Override
protected HttpClient buildHttpClient() throws Exception {
return RatpackTracing.create(testing.getOpenTelemetry())
.instrumentHttpClient(HttpClient.of(Action.noop()));
}
@Override
protected HttpClient buildHttpClient(Action<? super HttpClientSpec> action) throws Exception {
return RatpackTracing.create(testing.getOpenTelemetry())
.instrumentHttpClient(HttpClient.of(action));
}
}