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:
parent
69fc3df19a
commit
95e240c3e1
|
@ -1,6 +1,5 @@
|
|||
plugins {
|
||||
id("otel.library-instrumentation")
|
||||
id("otel.nullaway-conventions")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue