diff --git a/instrumentation/ratpack/ratpack-1.7/library/build.gradle.kts b/instrumentation/ratpack/ratpack-1.7/library/build.gradle.kts index 2643318cb9..7386823c00 100644 --- a/instrumentation/ratpack/ratpack-1.7/library/build.gradle.kts +++ b/instrumentation/ratpack/ratpack-1.7/library/build.gradle.kts @@ -1,6 +1,5 @@ plugins { id("otel.library-instrumentation") - id("otel.nullaway-conventions") } dependencies { diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/OpenTelemetryExecInitializer.java b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/OpenTelemetryExecInitializer.java new file mode 100644 index 0000000000..5ae07f75b8 --- /dev/null +++ b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/OpenTelemetryExecInitializer.java @@ -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); + } +} diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/OpenTelemetryHttpClient.java b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/OpenTelemetryHttpClient.java new file mode 100644 index 0000000000..a221d6160c --- /dev/null +++ b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/OpenTelemetryHttpClient.java @@ -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 instrumenter; + + OpenTelemetryHttpClient(Instrumenter 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); + }); + }); + } +} diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackHttpClientAttributesExtractor.java b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackHttpClientAttributesExtractor.java new file mode 100644 index 0000000000..9b34b30148 --- /dev/null +++ b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackHttpClientAttributesExtractor.java @@ -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 { + + 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 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 responseHeader( + RequestSpec requestSpec, HttpResponse httpResponse, String name) { + return httpResponse.getHeaders().getAll(name); + } +} diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackTracing.java b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackTracing.java index 2b9de88c31..9f7f3f1ce2 100644 --- a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackTracing.java +++ b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackTracing.java @@ -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 serverInstrumenter) { + RatpackTracing( + Instrumenter serverInstrumenter, + Instrumenter 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); } } diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackTracingBuilder.java b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackTracingBuilder.java index c0a350a8c6..74b7bacdf5 100644 --- a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackTracingBuilder.java +++ b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RatpackTracingBuilder.java @@ -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> + additionalHttpClientExtractors = new ArrayList<>(); + RatpackTracingBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; } @@ -44,6 +50,12 @@ public final class RatpackTracingBuilder { return this; } + public RatpackTracingBuilder addClientAttributeExtractor( + AttributesExtractor 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 httpClientInstrumenter() { + RatpackHttpNetAttributesExtractor netAttributes = new RatpackHttpNetAttributesExtractor(); + RatpackHttpClientAttributesExtractor httpAttributes = + new RatpackHttpClientAttributesExtractor(capturedHttpHeaders); + + return Instrumenter.builder( + openTelemetry, INSTRUMENTATION_NAME, HttpSpanNameExtractor.create(httpAttributes)) + .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributes)) + .addAttributesExtractor(netAttributes) + .addAttributesExtractor(httpAttributes) + .addAttributesExtractors(additionalHttpClientExtractors) + .addRequestMetrics(HttpServerMetrics.get()) + .newClientInstrumenter(RequestHeaderSetter.INSTANCE); } } diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RequestHeaderSetter.java b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RequestHeaderSetter.java new file mode 100644 index 0000000000..9ff9c2a23f --- /dev/null +++ b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/RequestHeaderSetter.java @@ -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 { + INSTANCE; + + @Override + public void set(@Nullable RequestSpec carrier, String key, String value) { + if (carrier != null) { + carrier.getHeaders().set(key, value); + } + } +} diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/internal/ContextHolder.java b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/internal/ContextHolder.java new file mode 100644 index 0000000000..c1839e091a --- /dev/null +++ b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/internal/ContextHolder.java @@ -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; + } +} diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/internal/RatpackHttpNetAttributesExtractor.java b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/internal/RatpackHttpNetAttributesExtractor.java new file mode 100644 index 0000000000..b2da2ff9a5 --- /dev/null +++ b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/internal/RatpackHttpNetAttributesExtractor.java @@ -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 { + @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; + } +} diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/RatpackFunctionalTest.groovy b/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/RatpackFunctionalTest.groovy index 6d4bf8fc83..91c61c9836 100644 --- a/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/RatpackFunctionalTest.groovy +++ b/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/RatpackFunctionalTest.groovy @@ -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")) + }) } } diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/client/InstrumentedHttpClientTest.groovy b/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/client/InstrumentedHttpClientTest.groovy new file mode 100644 index 0000000000..1cda8adaf7 --- /dev/null +++ b/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/client/InstrumentedHttpClientTest.groovy @@ -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 + } + } +} diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/server/RatpackServerApplicationTest.groovy b/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/server/RatpackServerApplicationTest.groovy index a867322a25..95222d7715 100644 --- a/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/server/RatpackServerApplicationTest.groovy +++ b/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/server/RatpackServerApplicationTest.groovy @@ -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") } + } } } } } - diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/test/java/io/opentelemetry/instrumentation/ratpack/AbstractRatpackHttpClientTest.java b/instrumentation/ratpack/ratpack-1.7/library/src/test/java/io/opentelemetry/instrumentation/ratpack/AbstractRatpackHttpClientTest.java new file mode 100644 index 0000000000..4353940eda --- /dev/null +++ b/instrumentation/ratpack/ratpack-1.7/library/src/test/java/io/opentelemetry/instrumentation/ratpack/AbstractRatpackHttpClientTest.java @@ -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 { + + 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 action) throws Exception { + return HttpClient.of(action); + } + + @Override + protected final Void buildRequest(String method, URI uri, Map headers) { + return null; + } + + @Override + protected final int sendRequest(Void request, String method, URI uri, Map 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 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 internalSendRequest( + HttpClient client, String method, URI uri, Map headers) { + Promise 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(); + } +} diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/test/java/io/opentelemetry/instrumentation/ratpack/RatpackHttpClientTest.java b/instrumentation/ratpack/ratpack-1.7/library/src/test/java/io/opentelemetry/instrumentation/ratpack/RatpackHttpClientTest.java new file mode 100644 index 0000000000..a0ea3d12ca --- /dev/null +++ b/instrumentation/ratpack/ratpack-1.7/library/src/test/java/io/opentelemetry/instrumentation/ratpack/RatpackHttpClientTest.java @@ -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 action) throws Exception { + return RatpackTracing.create(testing.getOpenTelemetry()) + .instrumentHttpClient(HttpClient.of(action)); + } +}