From 695cf0ad5fdc8aa8b9300ae7bb1dba065b2e4c99 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Wed, 28 Jul 2021 12:18:45 +0900 Subject: [PATCH] Add library instrumentation for Apache HTTPClient 4.3 (#3623) * Add apache httpclient 4.3 library instrumentation. * Fixup * Mostly done * Finish * Finish * Update instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientRequest.java Co-authored-by: Lauri Tulmin * Cleanup Co-authored-by: Lauri Tulmin --- .../library/build.gradle.kts | 14 ++ ...acheHttpClientHttpAttributesExtractor.java | 105 +++++++++++ ...pacheHttpClientNetAttributesExtractor.java | 38 ++++ .../v4_3/ApacheHttpClientRequest.java | 135 ++++++++++++++ .../v4_3/ApacheHttpClientTracing.java | 52 ++++++ .../v4_3/ApacheHttpClientTracingBuilder.java | 70 +++++++ .../v4_3/HttpHeaderSetter.java | 21 +++ .../v4_3/TracingHttpClientBuilder.java | 30 +++ .../v4_3/TracingProtocolExec.java | 175 ++++++++++++++++++ .../ApacheClientHostRequestContextTest.groovy | 28 +++ .../v4_3/ApacheClientHostRequestTest.groovy | 28 +++ .../ApacheClientUriRequestContextTest.groovy | 28 +++ .../v4_3/ApacheClientUriRequestTest.groovy | 28 +++ .../testing/build.gradle.kts | 13 ++ .../v4_3/ApacheHttpClientTest.groovy | 174 +++++++++++++++++ .../v4_3/HttpUriRequest.groovy | 23 +++ settings.gradle.kts | 2 + 17 files changed, 964 insertions(+) create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/build.gradle.kts create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientHttpAttributesExtractor.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientNetAttributesExtractor.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientRequest.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTracing.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTracingBuilder.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpHeaderSetter.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/TracingHttpClientBuilder.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/TracingProtocolExec.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientHostRequestContextTest.groovy create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientHostRequestTest.groovy create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientUriRequestContextTest.groovy create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientUriRequestTest.groovy create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/testing/build.gradle.kts create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/testing/src/main/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTest.groovy create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/testing/src/main/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpUriRequest.groovy diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/build.gradle.kts b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/build.gradle.kts new file mode 100644 index 0000000000..36958d1b11 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("otel.library-instrumentation") + id("otel.nullaway-conventions") +} + +dependencies { + library("org.apache.httpcomponents:httpclient:4.3") + + implementation("org.slf4j:slf4j-api") + + testImplementation(project(":instrumentation:apache-httpclient:apache-httpclient-4.3:testing")) + + latestDepTestLibrary("org.apache.httpcomponents:httpclient:4.+") +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientHttpAttributesExtractor.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientHttpAttributesExtractor.java new file mode 100644 index 0000000000..28198de86a --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientHttpAttributesExtractor.java @@ -0,0 +1,105 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3; + +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import org.apache.http.HttpResponse; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class ApacheHttpClientHttpAttributesExtractor + extends HttpAttributesExtractor { + + @Override + protected String method(ApacheHttpClientRequest request) { + return request.getMethod(); + } + + @Override + @Nullable + protected String url(ApacheHttpClientRequest request) { + return request.getUrl(); + } + + @Override + @Nullable + protected String target(ApacheHttpClientRequest request) { + return request.getTarget(); + } + + @Override + @Nullable + protected String host(ApacheHttpClientRequest request) { + return request.getHeader("Host"); + } + + @Override + @Nullable + protected String scheme(ApacheHttpClientRequest request) { + return request.getScheme(); + } + + @Override + @Nullable + protected String userAgent(ApacheHttpClientRequest request) { + return request.getHeader("User-Agent"); + } + + @Override + @Nullable + protected Long requestContentLength( + ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return null; + } + + @Override + @Nullable + protected Long requestContentLengthUncompressed( + ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return null; + } + + @Override + protected Integer statusCode(ApacheHttpClientRequest request, HttpResponse response) { + return response.getStatusLine().getStatusCode(); + } + + @Override + @Nullable + protected String flavor(ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return request.getFlavor(); + } + + @Override + @Nullable + protected Long responseContentLength(ApacheHttpClientRequest request, HttpResponse response) { + return null; + } + + @Override + @Nullable + protected Long responseContentLengthUncompressed( + ApacheHttpClientRequest request, HttpResponse response) { + return null; + } + + @Override + @Nullable + protected String serverName(ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return null; + } + + @Override + @Nullable + protected String route(ApacheHttpClientRequest request) { + return null; + } + + @Override + @Nullable + protected String clientIp(ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return null; + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientNetAttributesExtractor.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientNetAttributesExtractor.java new file mode 100644 index 0000000000..69458f393c --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientNetAttributesExtractor.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.apache.http.HttpResponse; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class ApacheHttpClientNetAttributesExtractor + extends NetAttributesExtractor { + + @Override + public String transport(ApacheHttpClientRequest request) { + return SemanticAttributes.NetTransportValues.IP_TCP; + } + + @Override + @Nullable + public String peerName(ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return request.getPeerName(); + } + + @Override + @Nullable + public Integer peerPort(ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return request.getPeerPort(); + } + + @Override + @Nullable + public String peerIp(ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return null; + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientRequest.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientRequest.java new file mode 100644 index 0000000000..9a28deeaf4 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientRequest.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3; + +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.URI; +import java.net.URISyntaxException; +import org.apache.http.Header; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.methods.HttpUriRequest; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ApacheHttpClientRequest { + + private static final Logger logger = LoggerFactory.getLogger(ApacheHttpClientRequest.class); + + @Nullable private final URI uri; + + private final HttpRequest delegate; + + ApacheHttpClientRequest(@Nullable HttpHost httpHost, HttpRequest httpRequest) { + URI calculatedUri = null; + if (httpRequest instanceof HttpUriRequest) { + calculatedUri = ((HttpUriRequest) httpRequest).getURI(); + } + if (calculatedUri == null && httpHost != null) { + try { + calculatedUri = new URI(httpHost.toURI() + httpRequest.getRequestLine().getUri()); + } catch (URISyntaxException e) { + // Ignore + } + } + uri = calculatedUri; + delegate = httpRequest; + } + + /** Returns the actual {@link HttpRequest} being executed by the client. */ + public HttpRequest getDelegate() { + return delegate; + } + + @Nullable + String getHeader(String name) { + Header header = delegate.getFirstHeader(name); + return header != null ? header.getValue() : null; + } + + void setHeader(String name, String value) { + delegate.setHeader(name, value); + } + + String getMethod() { + return delegate.getRequestLine().getMethod(); + } + + @Nullable + String getUrl() { + return uri != null ? uri.toString() : null; + } + + @Nullable + String getTarget() { + if (uri == null) { + return null; + } + String pathString = uri.getPath(); + String queryString = uri.getQuery(); + if (pathString != null && queryString != null) { + return pathString + "?" + queryString; + } else if (queryString != null) { + return "?" + queryString; + } else { + return pathString; + } + } + + @Nullable + String getScheme() { + return uri != null ? uri.getScheme() : null; + } + + @Nullable + String getFlavor() { + ProtocolVersion protocolVersion = delegate.getProtocolVersion(); + String protocol = protocolVersion.getProtocol(); + if (!protocol.equals("HTTP")) { + return null; + } + int major = protocolVersion.getMajor(); + int minor = protocolVersion.getMinor(); + if (major == 1 && minor == 0) { + return SemanticAttributes.HttpFlavorValues.HTTP_1_0; + } + if (major == 1 && minor == 1) { + return SemanticAttributes.HttpFlavorValues.HTTP_1_1; + } + if (major == 2 && minor == 0) { + return SemanticAttributes.HttpFlavorValues.HTTP_2_0; + } + logger.debug("unexpected http protocol version: {}", protocolVersion); + return null; + } + + @Nullable + String getPeerName() { + return uri != null ? uri.getHost() : null; + } + + @Nullable + Integer getPeerPort() { + if (uri == null) { + return null; + } + int port = uri.getPort(); + if (port != -1) { + return port; + } + switch (uri.getScheme()) { + case "http": + return 80; + case "https": + return 443; + default: + logger.debug("no default port mapping for scheme: {}", uri.getScheme()); + return null; + } + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTracing.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTracing.java new file mode 100644 index 0000000000..edd3c72124 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTracing.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import org.apache.http.HttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; + +/** Entrypoint for tracing Apache HTTP Client. */ +public final class ApacheHttpClientTracing { + + /** + * Returns a new {@link ApacheHttpClientTracing} configured with the given {@link OpenTelemetry}. + */ + public static ApacheHttpClientTracing create(OpenTelemetry openTelemetry) { + return newBuilder(openTelemetry).build(); + } + + /** + * Returns a new {@link ApacheHttpClientTracingBuilder} configured with the given {@link + * OpenTelemetry}. + */ + public static ApacheHttpClientTracingBuilder newBuilder(OpenTelemetry openTelemetry) { + return new ApacheHttpClientTracingBuilder(openTelemetry); + } + + private final Instrumenter instrumenter; + private final ContextPropagators propagators; + + ApacheHttpClientTracing( + Instrumenter instrumenter, + ContextPropagators propagators) { + this.instrumenter = instrumenter; + this.propagators = propagators; + } + + /** Returns a new {@link CloseableHttpClient} with tracing configured. */ + public CloseableHttpClient newHttpClient() { + return newHttpClientBuilder().build(); + } + + /** Returns a new {@link HttpClientBuilder} to create a client with tracing configured. */ + public HttpClientBuilder newHttpClientBuilder() { + return new TracingHttpClientBuilder(instrumenter, propagators); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTracingBuilder.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTracingBuilder.java new file mode 100644 index 0000000000..8d3261d8e5 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTracingBuilder.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; +import java.util.ArrayList; +import java.util.List; +import org.apache.http.HttpResponse; + +/** A builder for {@link ApacheHttpClientTracing}. */ +public final class ApacheHttpClientTracingBuilder { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.apache-httpclient-4.3"; + + private final OpenTelemetry openTelemetry; + + private final List> + additionalExtractors = new ArrayList<>(); + + ApacheHttpClientTracingBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** + * Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented + * items. The {@link AttributesExtractor} will be executed after all default extractors. + */ + public ApacheHttpClientTracingBuilder addAttributeExtractor( + AttributesExtractor + attributesExtractor) { + additionalExtractors.add(attributesExtractor); + return this; + } + + /** + * Returns a new {@link ApacheHttpClientTracing} configured with this {@link + * ApacheHttpClientTracingBuilder}. + */ + public ApacheHttpClientTracing build() { + HttpAttributesExtractor httpAttributesExtractor = + new ApacheHttpClientHttpAttributesExtractor(); + SpanNameExtractor spanNameExtractor = + HttpSpanNameExtractor.create(httpAttributesExtractor); + SpanStatusExtractor spanStatusExtractor = + HttpSpanStatusExtractor.create(httpAttributesExtractor); + ApacheHttpClientNetAttributesExtractor netAttributesExtractor = + new ApacheHttpClientNetAttributesExtractor(); + Instrumenter instrumenter = + Instrumenter.newBuilder( + openTelemetry, INSTRUMENTATION_NAME, spanNameExtractor) + .setSpanStatusExtractor(spanStatusExtractor) + .addAttributesExtractor(httpAttributesExtractor) + .addAttributesExtractor(netAttributesExtractor) + // We manually inject because we need to inject internal requests for redirects. + .newInstrumenter(SpanKindExtractor.alwaysClient()); + + return new ApacheHttpClientTracing(instrumenter, openTelemetry.getPropagators()); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpHeaderSetter.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpHeaderSetter.java new file mode 100644 index 0000000000..6c32374fde --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpHeaderSetter.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3; + +import io.opentelemetry.context.propagation.TextMapSetter; +import org.apache.http.client.methods.HttpRequestWrapper; +import org.checkerframework.checker.nullness.qual.Nullable; + +enum HttpHeaderSetter implements TextMapSetter { + INSTANCE; + + @Override + public void set(@Nullable HttpRequestWrapper carrier, String key, String value) { + if (carrier != null) { + carrier.setHeader(key, value); + } + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/TracingHttpClientBuilder.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/TracingHttpClientBuilder.java new file mode 100644 index 0000000000..a615b09194 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/TracingHttpClientBuilder.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3; + +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import org.apache.http.HttpResponse; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.execchain.ClientExecChain; + +final class TracingHttpClientBuilder extends HttpClientBuilder { + + private final Instrumenter instrumenter; + private final ContextPropagators propagators; + + TracingHttpClientBuilder( + Instrumenter instrumenter, + ContextPropagators propagators) { + this.instrumenter = instrumenter; + this.propagators = propagators; + } + + @Override + protected ClientExecChain decorateProtocolExec(ClientExecChain protocolExec) { + return new TracingProtocolExec(instrumenter, propagators, protocolExec); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/TracingProtocolExec.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/TracingProtocolExec.java new file mode 100644 index 0000000000..0fcae95841 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/TracingProtocolExec.java @@ -0,0 +1,175 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.io.IOException; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.ProtocolException; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpExecutionAware; +import org.apache.http.client.methods.HttpRequestWrapper; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.impl.client.DefaultRedirectStrategy; +import org.apache.http.impl.client.RedirectLocations; +import org.apache.http.impl.execchain.ClientExecChain; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class TracingProtocolExec implements ClientExecChain { + + private static final String REQUEST_CONTEXT_ATTRIBUTE_ID = + TracingProtocolExec.class.getName() + ".context"; + private static final String REQUEST_WRAPPER_ATTRIBUTE_ID = + TracingProtocolExec.class.getName() + ".requestWrapper"; + private static final String REDIRECT_COUNT_ATTRIBUTE_ID = + TracingProtocolExec.class.getName() + ".redirectCount"; + + private final Instrumenter instrumenter; + private final ContextPropagators propagators; + private final ClientExecChain exec; + + TracingProtocolExec( + Instrumenter instrumenter, + ContextPropagators propagators, + ClientExecChain exec) { + this.instrumenter = instrumenter; + this.propagators = propagators; + this.exec = exec; + } + + @Override + public CloseableHttpResponse execute( + HttpRoute route, + HttpRequestWrapper request, + HttpClientContext httpContext, + HttpExecutionAware httpExecutionAware) + throws IOException, HttpException { + Context context = httpContext.getAttribute(REQUEST_CONTEXT_ATTRIBUTE_ID, Context.class); + if (context != null) { + ApacheHttpClientRequest instrumenterRequest = + httpContext.getAttribute(REQUEST_WRAPPER_ATTRIBUTE_ID, ApacheHttpClientRequest.class); + // Request already had a context so a redirect. Don't create a new span just inject and + // execute. + propagators.getTextMapPropagator().inject(context, request, HttpHeaderSetter.INSTANCE); + return execute(route, request, instrumenterRequest, httpContext, httpExecutionAware, context); + } + + HttpHost host = null; + if (route.getTargetHost() != null) { + host = route.getTargetHost(); + } else if (httpContext.getTargetHost() != null) { + host = httpContext.getTargetHost(); + } + if (host != null) { + if ((host.getSchemeName().equals("https") && host.getPort() == 443) + || (host.getSchemeName().equals("http") && host.getPort() == 80)) { + // port seems to be added to the host by route planning for standard ports even if not + // specified in the URL. There doesn't seem to be a way to differentiate between explicit + // and implicit port, but ignore in both cases to match the more common case. + host = new HttpHost(host.getHostName(), -1, host.getSchemeName()); + } + } + ApacheHttpClientRequest instrumenterRequest = new ApacheHttpClientRequest(host, request); + + Context parentContext = Context.current(); + if (!instrumenter.shouldStart(parentContext, instrumenterRequest)) { + return exec.execute(route, request, httpContext, httpExecutionAware); + } + + context = instrumenter.start(parentContext, instrumenterRequest); + httpContext.setAttribute(REQUEST_CONTEXT_ATTRIBUTE_ID, context); + httpContext.setAttribute(REQUEST_WRAPPER_ATTRIBUTE_ID, instrumenterRequest); + httpContext.setAttribute(REDIRECT_COUNT_ATTRIBUTE_ID, 0); + + propagators.getTextMapPropagator().inject(context, request, HttpHeaderSetter.INSTANCE); + + return execute(route, request, instrumenterRequest, httpContext, httpExecutionAware, context); + } + + private CloseableHttpResponse execute( + HttpRoute route, + HttpRequestWrapper request, + ApacheHttpClientRequest instrumenterRequest, + HttpClientContext httpContext, + HttpExecutionAware httpExecutionAware, + Context context) + throws IOException, HttpException { + CloseableHttpResponse response = null; + Throwable error = null; + try (Scope ignored = context.makeCurrent()) { + response = exec.execute(route, request, httpContext, httpExecutionAware); + return response; + } catch (Throwable e) { + error = e; + throw e; + } finally { + if (!pendingRedirect(context, httpContext, request, instrumenterRequest, response)) { + instrumenter.end(context, instrumenterRequest, response, error); + } + } + } + + private boolean pendingRedirect( + Context context, + HttpClientContext httpContext, + HttpRequestWrapper request, + ApacheHttpClientRequest instrumenterRequest, + @Nullable CloseableHttpResponse response) { + if (response == null) { + return false; + } + if (!httpContext.getRequestConfig().isRedirectsEnabled()) { + return false; + } + + // TODO(anuraaga): Support redirect strategies other than the default. There is no way to get + // the user defined redirect strategy without some tricks, but it's very rare to override + // the strategy, usually it is either on or off as checked above. We can add support for this + // later if needed. + try { + if (!DefaultRedirectStrategy.INSTANCE.isRedirected(request, response, httpContext)) { + return false; + } + } catch (ProtocolException e) { + // DefaultRedirectStrategy.isRedirected cannot throw this so just return a default. + return false; + } + + // Very hacky and a bit slow, but the only way to determine whether the client will fail with + // a circular redirect, which happens before exec decorators run. + RedirectLocations redirectLocations = + (RedirectLocations) httpContext.getAttribute(HttpClientContext.REDIRECT_LOCATIONS); + if (redirectLocations != null) { + RedirectLocations copy = new RedirectLocations(); + copy.addAll(redirectLocations); + + try { + DefaultRedirectStrategy.INSTANCE.getLocationURI(request, response, httpContext); + } catch (ProtocolException e) { + // We will not be returning to the Exec, finish the span. + instrumenter.end(context, instrumenterRequest, response, new ClientProtocolException(e)); + return true; + } finally { + httpContext.setAttribute(HttpClientContext.REDIRECT_LOCATIONS, copy); + } + } + + int redirectCount = httpContext.getAttribute(REDIRECT_COUNT_ATTRIBUTE_ID, Integer.class); + if (++redirectCount > httpContext.getRequestConfig().getMaxRedirects()) { + return false; + } + + httpContext.setAttribute(REDIRECT_COUNT_ATTRIBUTE_ID, redirectCount); + return true; + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientHostRequestContextTest.groovy b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientHostRequestContextTest.groovy new file mode 100644 index 0000000000..7b50ccf311 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientHostRequestContextTest.groovy @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3 + +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import org.apache.http.client.config.RequestConfig +import org.apache.http.impl.client.CloseableHttpClient + +class ApacheClientHostRequestContextTest extends AbstractApacheClientHostRequestContextTest implements LibraryTestTrait { + @Override + protected CloseableHttpClient createClient() { + def builder = ApacheHttpClientTracing.create(openTelemetry).newHttpClientBuilder() + builder.defaultRequestConfig = RequestConfig.custom() + .setMaxRedirects(maxRedirects()) + .setConnectTimeout(CONNECT_TIMEOUT_MS) + .build() + return builder.build() + } + + // library instrumentation doesn't have a good way of suppressing nested CLIENT spans yet + @Override + boolean testWithClientParent() { + false + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientHostRequestTest.groovy b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientHostRequestTest.groovy new file mode 100644 index 0000000000..35820cf14a --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientHostRequestTest.groovy @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3 + +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import org.apache.http.client.config.RequestConfig +import org.apache.http.impl.client.CloseableHttpClient + +class ApacheClientHostRequestTest extends AbstractApacheClientHostRequestTest implements LibraryTestTrait { + @Override + protected CloseableHttpClient createClient() { + def builder = ApacheHttpClientTracing.create(openTelemetry).newHttpClientBuilder() + builder.defaultRequestConfig = RequestConfig.custom() + .setMaxRedirects(maxRedirects()) + .setConnectTimeout(CONNECT_TIMEOUT_MS) + .build() + return builder.build() + } + + // library instrumentation doesn't have a good way of suppressing nested CLIENT spans yet + @Override + boolean testWithClientParent() { + false + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientUriRequestContextTest.groovy b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientUriRequestContextTest.groovy new file mode 100644 index 0000000000..48e21255c2 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientUriRequestContextTest.groovy @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3 + +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import org.apache.http.client.config.RequestConfig +import org.apache.http.impl.client.CloseableHttpClient + +class ApacheClientUriRequestContextTest extends AbstractApacheClientUriRequestContextTest implements LibraryTestTrait { + @Override + protected CloseableHttpClient createClient() { + def builder = ApacheHttpClientTracing.create(openTelemetry).newHttpClientBuilder() + builder.defaultRequestConfig = RequestConfig.custom() + .setMaxRedirects(maxRedirects()) + .setConnectTimeout(CONNECT_TIMEOUT_MS) + .build() + return builder.build() + } + + // library instrumentation doesn't have a good way of suppressing nested CLIENT spans yet + @Override + boolean testWithClientParent() { + false + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientUriRequestTest.groovy b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientUriRequestTest.groovy new file mode 100644 index 0000000000..488983d6a9 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheClientUriRequestTest.groovy @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3 + +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import org.apache.http.client.config.RequestConfig +import org.apache.http.impl.client.CloseableHttpClient + +class ApacheClientUriRequestTest extends AbstractApacheClientUriRequestTest implements LibraryTestTrait { + @Override + protected CloseableHttpClient createClient() { + def builder = ApacheHttpClientTracing.create(openTelemetry).newHttpClientBuilder() + builder.defaultRequestConfig = RequestConfig.custom() + .setMaxRedirects(maxRedirects()) + .setConnectTimeout(CONNECT_TIMEOUT_MS) + .build() + return builder.build() + } + + // library instrumentation doesn't have a good way of suppressing nested CLIENT spans yet + @Override + boolean testWithClientParent() { + false + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/build.gradle.kts b/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/build.gradle.kts new file mode 100644 index 0000000000..2cdd0fddb5 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + api(project(":testing-common")) + + api("org.apache.httpcomponents:httpclient:4.3") + + implementation("org.codehaus.groovy:groovy-all") + implementation("io.opentelemetry:opentelemetry-api") + implementation("org.spockframework:spock-core") +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/src/main/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTest.groovy b/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/src/main/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTest.groovy new file mode 100644 index 0000000000..094e95ef1a --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/src/main/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTest.groovy @@ -0,0 +1,174 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3 + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.function.Consumer +import org.apache.http.HttpHost +import org.apache.http.HttpRequest +import org.apache.http.HttpResponse +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.message.BasicHeader +import org.apache.http.message.BasicHttpRequest +import org.apache.http.protocol.BasicHttpContext +import spock.lang.Shared + +abstract class ApacheHttpClientTest extends HttpClientTest { + + abstract protected CloseableHttpClient createClient() + + @Override + Integer responseCodeOnRedirectError() { + return 302 + } + + @Shared + CloseableHttpClient client = createClient() + + @Override + boolean testCausality() { + false + } + + @Override + T buildRequest(String method, URI uri, Map headers) { + def request = createRequest(method, uri) + headers.entrySet().each { + request.setHeader(new BasicHeader(it.key, it.value)) + } + return request + } + + @Override + Set> httpAttributes(URI uri) { + Set> extra = [ + SemanticAttributes.HTTP_SCHEME, + SemanticAttributes.HTTP_TARGET + ] + super.httpAttributes(uri) + extra + } + + // compilation fails with @Override annotation on this method (groovy quirk?) + int sendRequest(T request, String method, URI uri, Map headers) { + def response = executeRequest(request, uri) + response.entity?.content?.close() // Make sure the connection is closed. + return response.statusLine.statusCode + } + + // compilation fails with @Override annotation on this method (groovy quirk?) + void sendRequestWithCallback(T request, String method, URI uri, Map headers, RequestResult requestResult) { + try { + executeRequestWithCallback(request, uri) { + it.entity?.content?.close() // Make sure the connection is closed. + requestResult.complete(it.statusLine.statusCode) + } + } catch (Throwable throwable) { + requestResult.complete(throwable) + } + } + + abstract T createRequest(String method, URI uri) + + abstract HttpResponse executeRequest(T request, URI uri) + + abstract void executeRequestWithCallback(T request, URI uri, Consumer callback) + + static String fullPathFromURI(URI uri) { + StringBuilder builder = new StringBuilder() + if (uri.getPath() != null) { + builder.append(uri.getPath()) + } + + if (uri.getQuery() != null) { + builder.append('?') + builder.append(uri.getQuery()) + } + + if (uri.getFragment() != null) { + builder.append('#') + builder.append(uri.getFragment()) + } + return builder.toString() + } +} + +abstract class AbstractApacheClientHostRequestTest extends ApacheHttpClientTest { + @Override + BasicHttpRequest createRequest(String method, URI uri) { + return new BasicHttpRequest(method, fullPathFromURI(uri)) + } + + @Override + HttpResponse executeRequest(BasicHttpRequest request, URI uri) { + return client.execute(new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), request) + } + + @Override + void executeRequestWithCallback(BasicHttpRequest request, URI uri, Consumer callback) { + client.execute(new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), request) { + callback.accept(it) + } + } +} + +abstract class AbstractApacheClientHostRequestContextTest extends ApacheHttpClientTest { + @Override + BasicHttpRequest createRequest(String method, URI uri) { + return new BasicHttpRequest(method, fullPathFromURI(uri)) + } + + @Override + HttpResponse executeRequest(BasicHttpRequest request, URI uri) { + return client.execute(new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), request, new BasicHttpContext()) + } + + @Override + void executeRequestWithCallback(BasicHttpRequest request, URI uri, Consumer callback) { + client.execute(new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), request, { + callback.accept(it) + }, new BasicHttpContext()) + } +} + +abstract class AbstractApacheClientUriRequestTest extends ApacheHttpClientTest { + @Override + HttpUriRequest createRequest(String method, URI uri) { + return new HttpUriRequest(method, uri) + } + + @Override + HttpResponse executeRequest(HttpUriRequest request, URI uri) { + return client.execute(request) + } + + @Override + void executeRequestWithCallback(HttpUriRequest request, URI uri, Consumer callback) { + client.execute(request) { + callback.accept(it) + } + } +} + +abstract class AbstractApacheClientUriRequestContextTest extends ApacheHttpClientTest { + @Override + HttpUriRequest createRequest(String method, URI uri) { + return new HttpUriRequest(method, uri) + } + + @Override + HttpResponse executeRequest(HttpUriRequest request, URI uri) { + return client.execute(request, new BasicHttpContext()) + } + + @Override + void executeRequestWithCallback(HttpUriRequest request, URI uri, Consumer callback) { + client.execute(request, { + callback.accept(it) + }, new BasicHttpContext()) + } +} \ No newline at end of file diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/src/main/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpUriRequest.groovy b/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/src/main/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpUriRequest.groovy new file mode 100644 index 0000000000..54c5bf6887 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/src/main/groovy/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpUriRequest.groovy @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3 + +import org.apache.http.client.methods.HttpRequestBase + +class HttpUriRequest extends HttpRequestBase { + + private final String methodName + + HttpUriRequest(final String methodName, final URI uri) { + this.methodName = methodName + setURI(uri) + } + + @Override + String getMethod() { + return methodName + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index a2f7a973c5..edc6eec899 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -90,6 +90,8 @@ include(":instrumentation:apache-dubbo-2.7:testing") include(":instrumentation:apache-httpasyncclient-4.1:javaagent") include(":instrumentation:apache-httpclient:apache-httpclient-2.0:javaagent") include(":instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent") +include(":instrumentation:apache-httpclient:apache-httpclient-4.3:library") +include(":instrumentation:apache-httpclient:apache-httpclient-4.3:testing") include(":instrumentation:apache-httpclient:apache-httpclient-5.0:javaagent") include(":instrumentation:armeria-1.3:javaagent") include(":instrumentation:armeria-1.3:library")