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 { plugins {
id("otel.library-instrumentation") id("otel.library-instrumentation")
id("otel.nullaway-conventions")
} }
dependencies { 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.api.OpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import ratpack.exec.ExecInitializer;
import ratpack.exec.ExecInterceptor; import ratpack.exec.ExecInterceptor;
import ratpack.handling.HandlerDecorator; import ratpack.handling.HandlerDecorator;
import ratpack.http.Request; import ratpack.http.Request;
import ratpack.http.Response; import ratpack.http.Response;
import ratpack.http.client.HttpClient;
import ratpack.http.client.HttpResponse;
import ratpack.http.client.RequestSpec;
import ratpack.registry.RegistrySpec; import ratpack.registry.RegistrySpec;
/** /**
@ -42,9 +46,13 @@ public final class RatpackTracing {
} }
private final OpenTelemetryServerHandler serverHandler; 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); serverHandler = new OpenTelemetryServerHandler(serverInstrumenter);
httpClientInstrumenter = new OpenTelemetryHttpClient(clientInstrumenter);
} }
/** Returns instance of {@link OpenTelemetryServerHandler} to support Ratpack Registry binding. */ /** Returns instance of {@link OpenTelemetryServerHandler} to support Ratpack Registry binding. */
@ -57,9 +65,20 @@ public final class RatpackTracing {
return OpenTelemetryExecInterceptor.INSTANCE; 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. */ /** Configures the {@link RegistrySpec} with OpenTelemetry. */
public void configureServerRegistry(RegistrySpec registry) { public void configureServerRegistry(RegistrySpec registry) {
registry.add(HandlerDecorator.prepend(serverHandler)); registry.add(HandlerDecorator.prepend(serverHandler));
registry.add(OpenTelemetryExecInterceptor.INSTANCE); 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.HttpServerMetrics;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor;
import io.opentelemetry.instrumentation.ratpack.internal.RatpackHttpNetAttributesExtractor;
import io.opentelemetry.instrumentation.ratpack.internal.RatpackNetAttributesExtractor; import io.opentelemetry.instrumentation.ratpack.internal.RatpackNetAttributesExtractor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import ratpack.http.Request; import ratpack.http.Request;
import ratpack.http.Response; import ratpack.http.Response;
import ratpack.http.client.HttpResponse;
import ratpack.http.client.RequestSpec;
/** A builder for {@link RatpackTracing}. */ /** A builder for {@link RatpackTracing}. */
public final class RatpackTracingBuilder { public final class RatpackTracingBuilder {
@ -30,6 +33,9 @@ public final class RatpackTracingBuilder {
new ArrayList<>(); new ArrayList<>();
private CapturedHttpHeaders capturedHttpHeaders = CapturedHttpHeaders.server(Config.get()); private CapturedHttpHeaders capturedHttpHeaders = CapturedHttpHeaders.server(Config.get());
private final List<AttributesExtractor<? super RequestSpec, ? super HttpResponse>>
additionalHttpClientExtractors = new ArrayList<>();
RatpackTracingBuilder(OpenTelemetry openTelemetry) { RatpackTracingBuilder(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry; this.openTelemetry = openTelemetry;
} }
@ -44,6 +50,12 @@ public final class RatpackTracingBuilder {
return this; 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 * Configure the instrumentation to capture chosen HTTP request and response headers as span
* attributes. * attributes.
@ -72,6 +84,21 @@ public final class RatpackTracingBuilder {
.addRequestMetrics(HttpServerMetrics.get()) .addRequestMetrics(HttpServerMetrics.get())
.newServerInstrumenter(RatpackGetter.INSTANCE); .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 package io.opentelemetry.instrumentation.ratpack
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter
import io.opentelemetry.sdk.trace.export.SpanExporter import io.opentelemetry.sdk.trace.export.SpanExporter
import ratpack.guice.BindingsImposition
import ratpack.impose.ForceDevelopmentImposition import ratpack.impose.ForceDevelopmentImposition
import ratpack.impose.ImpositionsSpec import ratpack.impose.ImpositionsSpec
import ratpack.impose.UserRegistryImposition import ratpack.impose.UserRegistryImposition
import ratpack.registry.Registry import ratpack.registry.Registry
import ratpack.test.MainClassApplicationUnderTest import ratpack.test.MainClassApplicationUnderTest
import ratpack.test.embed.EmbeddedApp
class RatpackFunctionalTest extends MainClassApplicationUnderTest { class RatpackFunctionalTest extends MainClassApplicationUnderTest {
Registry registry Registry registry
@Lazy InMemorySpanExporter spanExporter = registry.get(SpanExporter) as InMemorySpanExporter @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) { RatpackFunctionalTest(Class<?> mainClass) {
super(mainClass) super(mainClass)
@ -31,5 +37,8 @@ class RatpackFunctionalTest extends MainClassApplicationUnderTest {
registry = r registry = r
registry 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.SdkTracerProvider
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor
import io.opentelemetry.sdk.trace.export.SpanExporter import io.opentelemetry.sdk.trace.export.SpanExporter
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes import ratpack.exec.ExecInitializer
import ratpack.exec.ExecInterceptor import ratpack.exec.ExecInterceptor
import ratpack.guice.Guice import ratpack.guice.Guice
import ratpack.http.client.HttpClient
import ratpack.server.RatpackServer import ratpack.server.RatpackServer
import spock.lang.Specification import spock.lang.Specification
import spock.util.concurrent.PollingConditions import spock.util.concurrent.PollingConditions
import javax.inject.Singleton 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 { class RatpackServerApplicationTest extends Specification {
def app = new RatpackFunctionalTest(RatpackApp) def app = new RatpackFunctionalTest(RatpackApp)
@ -40,10 +46,35 @@ class RatpackServerApplicationTest extends Specification {
def attributes = spanData.attributes.asMap() def attributes = spanData.attributes.asMap()
spanData.kind == SpanKind.SERVER spanData.kind == SpanKind.SERVER
attributes[SemanticAttributes.HTTP_ROUTE] == "/foo" attributes[HTTP_ROUTE] == "/foo"
attributes[SemanticAttributes.HTTP_TARGET] == "/foo" attributes[HTTP_TARGET] == "/foo"
attributes[SemanticAttributes.HTTP_METHOD] == "GET" attributes[HTTP_METHOD] == "GET"
attributes[SemanticAttributes.HTTP_STATUS_CODE] == 200L 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() .build()
return OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).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 @CompileStatic
@ -99,19 +142,17 @@ class RatpackApp {
static void main(String... args) { static void main(String... args) {
RatpackServer.start { server -> RatpackServer.start { server ->
server server
.registry( .registry(Guice.registry { b -> b.module(OpenTelemetryModule) })
Guice.registry { bindings ->
bindings
.module(OpenTelemetryModule)
}
)
.handlers { chain -> .handlers { chain ->
chain chain
.get("ignore") { ctx -> ctx.render("ignored") } .get("ignore") { ctx -> ctx.render("ignored") }
.all(OpenTelemetryServerHandler) .all(OpenTelemetryServerHandler)
.get("foo") { ctx -> ctx.render("hi-foo") } .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));
}
}