diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/build.gradle b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/build.gradle new file mode 100644 index 000000000000..df5b5fc1fd21 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/build.gradle @@ -0,0 +1,28 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group = "org.eclipse.jetty" + module = 'jetty-client' + versions = "[9.2,9.4.+)" + } +} + + +//Jetty client 9.2 is the best starting point, HttpClient.send() is stable there +def jettyVers_base9 = '9.2.0.v20140526' + + +dependencies { + implementation project(':instrumentation:jetty-httpclient:jetty-httpclient-9.2:library') + + library "org.eclipse.jetty:jetty-client:${jettyVers_base9}" + latestDepTestLibrary "org.eclipse.jetty:jetty-client:9.+" + + testImplementation project(':instrumentation:jetty-httpclient:jetty-httpclient-9.2:testing') + testImplementation("org.eclipse.jetty:jetty-server:${jettyVers_base9}") { + exclude group: 'org.eclipse.jetty', module: 'jetty-client' + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9Instrumentation.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9Instrumentation.java new file mode 100644 index 000000000000..47e730d2abf6 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9Instrumentation.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.httpclient.v9_2; + +import static io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyClientWrapUtil.wrapResponseListeners; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.jetty.httpclient.v9_2.JettyHttpClientSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyHttpClient9TracingInterceptor; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.api.Response; + +public class JettyHttpClient9Instrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.eclipse.jetty.client.HttpClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("send")) + .and(takesArgument(0, named("org.eclipse.jetty.client.HttpRequest"))) + .and(takesArgument(1, List.class)), + JettyHttpClient9Instrumentation.class.getName() + "$JettyHttpClient9Advice"); + } + + @SuppressWarnings("unused") + public static class JettyHttpClient9Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void addTracingEnter( + @Advice.Argument(value = 0) HttpRequest httpRequest, + @Advice.Argument(value = 1, readOnly = false) List listeners, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!instrumenter().shouldStart(parentContext, httpRequest)) { + return; + } + + // First step is to attach the tracer to the Jetty request. Request listeners are wrapped here + JettyHttpClient9TracingInterceptor requestInterceptor = + new JettyHttpClient9TracingInterceptor(parentContext, instrumenter()); + requestInterceptor.attachToRequest(httpRequest); + + // Second step is to wrap all the important result callback + listeners = wrapResponseListeners(parentContext, listeners); + + context = requestInterceptor.getContext(); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exitTracingInterceptor( + @Advice.Argument(value = 0) HttpRequest httpRequest, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + if (scope == null) { + return; + } + + // not ending span here unless error, span ended in the interceptor + scope.close(); + if (throwable != null) { + instrumenter().end(context, httpRequest, null, throwable); + } + } + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9InstrumentationModule.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9InstrumentationModule.java new file mode 100644 index 000000000000..2321e5c9c072 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9InstrumentationModule.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.httpclient.v9_2; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class JettyHttpClient9InstrumentationModule extends InstrumentationModule { + + public JettyHttpClient9InstrumentationModule() { + super("jetty-httpclient", "jetty-httpclient-9.2"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new JettyHttpClient9Instrumentation()); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // AbstractTypedContentProvider showed up in version Jetty Client 9.2 on to 10.x + return hasClassesNamed("org.eclipse.jetty.client.util.AbstractTypedContentProvider"); + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClientSingletons.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClientSingletons.java new file mode 100644 index 000000000000..ee09856fb565 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClientSingletons.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.httpclient.v9_2; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyClientInstrumenterBuilder; +import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyHttpClientNetAttributesExtractor; +import io.opentelemetry.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; + +public class JettyHttpClientSingletons { + + private static final Instrumenter INSTRUMENTER; + + private JettyHttpClientSingletons() {} + + static { + JettyClientInstrumenterBuilder builder = + new JettyClientInstrumenterBuilder(GlobalOpenTelemetry.get()); + + PeerServiceAttributesExtractor peerServiceAttributesExtractor = + PeerServiceAttributesExtractor.create(new JettyHttpClientNetAttributesExtractor()); + INSTRUMENTER = builder.addAttributeExtractor(peerServiceAttributesExtractor).build(); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9AgentTest.groovy b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9AgentTest.groovy new file mode 100644 index 000000000000..85e7bb3d9f9d --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9AgentTest.groovy @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.httpclient.v9_2 + +import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.AbstractJettyClient9Test +import io.opentelemetry.instrumentation.test.AgentTestTrait +import org.eclipse.jetty.client.HttpClient +import org.eclipse.jetty.util.ssl.SslContextFactory + +class JettyHttpClient9AgentTest extends AbstractJettyClient9Test implements AgentTestTrait { + + @Override + HttpClient createStandardClient() { + return new HttpClient() + } + + @Override + HttpClient createHttpsClient(SslContextFactory sslContextFactory) { + return new HttpClient(sslContextFactory) + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/build.gradle b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/build.gradle new file mode 100644 index 000000000000..0dc360992f41 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/build.gradle @@ -0,0 +1,17 @@ +plugins { + id("otel.library-instrumentation") + id("net.ltgt.errorprone") +} + + +//Jetty client 9.2 is the best starting point, HttpClient.send() is stable there +def jettyVers_base9 = '9.2.0.v20140526' + +dependencies { + library "org.eclipse.jetty:jetty-client:${jettyVers_base9}" + latestDepTestLibrary "org.eclipse.jetty:jetty-client:9.+" + testImplementation project(':instrumentation:jetty-httpclient::jetty-httpclient-9.2:testing') + + implementation "org.slf4j:slf4j-api" +} + diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyClientTracing.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyClientTracing.java new file mode 100644 index 000000000000..8f9bdaeea61c --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyClientTracing.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v9_2; + +import io.opentelemetry.api.OpenTelemetry; +import org.eclipse.jetty.client.HttpClient; + +/** JettyClientTracing, the Entrypoint for tracing Jetty client. */ +public final class JettyClientTracing { + + private final HttpClient httpClient; + + public static JettyClientTracing create(OpenTelemetry openTelemetry) { + JettyClientTracingBuilder builder = newBuilder(openTelemetry); + return builder.build(); + } + + public static JettyClientTracingBuilder newBuilder(OpenTelemetry openTelemetry) { + return new JettyClientTracingBuilder(openTelemetry); + } + + public HttpClient getHttpClient() { + return httpClient; + } + + JettyClientTracing(HttpClient httpClient) { + this.httpClient = httpClient; + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyClientTracingBuilder.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyClientTracingBuilder.java new file mode 100644 index 000000000000..b18146ddfdea --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyClientTracingBuilder.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v9_2; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyClientInstrumenterBuilder; +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +public final class JettyClientTracingBuilder { + + private final OpenTelemetry openTelemetry; + private HttpClientTransport httpClientTransport; + private SslContextFactory sslContextFactory; + + public JettyClientTracingBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + public JettyClientTracingBuilder setHttpClientTransport(HttpClientTransport httpClientTransport) { + this.httpClientTransport = httpClientTransport; + return this; + } + + public JettyClientTracingBuilder setSslContextFactory(SslContextFactory sslContextFactory) { + this.sslContextFactory = sslContextFactory; + return this; + } + + public JettyClientTracing build() { + JettyClientInstrumenterBuilder instrumenterBuilder = + new JettyClientInstrumenterBuilder(this.openTelemetry); + Instrumenter instrumenter = instrumenterBuilder.build(); + + TracingHttpClient tracingHttpClient = + TracingHttpClient.buildNew(instrumenter, this.sslContextFactory, this.httpClientTransport); + + return new JettyClientTracing(tracingHttpClient); + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/TracingHttpClient.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/TracingHttpClient.java new file mode 100644 index 000000000000..094b38ced322 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/TracingHttpClient.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v9_2; + +import static io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyClientWrapUtil.wrapResponseListeners; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyHttpClient9TracingInterceptor; +import java.util.List; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +class TracingHttpClient extends HttpClient { + + private final Instrumenter instrumenter; + + TracingHttpClient(Instrumenter instrumenter) { + super(); + this.instrumenter = instrumenter; + } + + TracingHttpClient( + Instrumenter instrumenter, SslContextFactory sslContextFactory) { + super(sslContextFactory); + this.instrumenter = instrumenter; + } + + TracingHttpClient( + Instrumenter instrumenter, + HttpClientTransport transport, + SslContextFactory sslContextFactory) { + super(transport, sslContextFactory); + this.instrumenter = instrumenter; + } + + static TracingHttpClient buildNew( + Instrumenter instrumenter, + SslContextFactory sslContextFactory, + HttpClientTransport httpClientTransport) { + TracingHttpClient tracingHttpClient = null; + if (sslContextFactory != null && httpClientTransport != null) { + tracingHttpClient = + new TracingHttpClient(instrumenter, httpClientTransport, sslContextFactory); + } else if (sslContextFactory != null) { + tracingHttpClient = new TracingHttpClient(instrumenter, sslContextFactory); + } else { + tracingHttpClient = new TracingHttpClient(instrumenter); + } + return tracingHttpClient; + } + + @Override + protected void send(HttpRequest request, List listeners) { + Context parentContext = Context.current(); + JettyHttpClient9TracingInterceptor requestInterceptor = + new JettyHttpClient9TracingInterceptor(parentContext, this.instrumenter); + requestInterceptor.attachToRequest(request); + List wrapped = wrapResponseListeners(parentContext, listeners); + super.send(request, wrapped); + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/HttpHeaderSetter.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/HttpHeaderSetter.java new file mode 100644 index 000000000000..df486e125d76 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/HttpHeaderSetter.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal; + +import io.opentelemetry.context.propagation.TextMapSetter; +import org.eclipse.jetty.client.api.Request; + +final class HttpHeaderSetter implements TextMapSetter { + + @Override + public void set(Request request, String key, String value) { + if (request != null) { + // dedupe header fields here with a put() + request.getHeaders().put(key, value); + } + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientHttpAttributesExtractor.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientHttpAttributesExtractor.java new file mode 100644 index 000000000000..f39aa660ee00 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientHttpAttributesExtractor.java @@ -0,0 +1,156 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal; + +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HttpFlavorValues.HTTP_1_0; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HttpFlavorValues.HTTP_1_1; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HttpFlavorValues.HTTP_2_0; + +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpVersion; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class JettyClientHttpAttributesExtractor extends HttpAttributesExtractor { + private static final Logger LOG = + LoggerFactory.getLogger(JettyClientHttpAttributesExtractor.class); + + @Override + @Nullable + protected String method(Request request) { + return request.getMethod(); + } + + @Override + @Nullable + protected String url(Request request) { + return request.getURI().toString(); + } + + @Override + @Nullable + protected String target(Request request) { + String queryString = request.getQuery(); + return queryString != null ? request.getPath() + "?" + queryString : request.getPath(); + } + + @Override + @Nullable + protected String host(Request request) { + return request.getHost(); + } + + @Override + @Nullable + protected String route(Request request) { + return null; + } + + @Override + @Nullable + protected String scheme(Request request) { + return request.getScheme(); + } + + @Override + @Nullable + protected String userAgent(Request request) { + HttpField agentField = request.getHeaders().getField(HttpHeader.USER_AGENT); + return agentField != null ? agentField.getValue() : null; + } + + @Override + @Nullable + protected Long requestContentLength(Request request, @Nullable Response response) { + HttpField requestContentLengthField = request.getHeaders().getField(HttpHeader.CONTENT_LENGTH); + return getLongFromJettyHttpField(requestContentLengthField); + } + + @Override + @Nullable + protected Long requestContentLengthUncompressed(Request request, @Nullable Response response) { + return null; + } + + @Override + @Nullable + protected String flavor(Request request, @Nullable Response response) { + + if (response == null) { + return HTTP_1_1; + } + HttpVersion httpVersion = response.getVersion(); + httpVersion = (httpVersion != null) ? httpVersion : HttpVersion.HTTP_1_1; + switch (httpVersion) { + case HTTP_0_9: + case HTTP_1_0: + return HTTP_1_0; + case HTTP_1_1: + return HTTP_1_1; + default: + // version 2.0 enum name difference in later versions 9.2 and 9.4 versions + if (httpVersion.toString().endsWith("2.0")) { + return HTTP_2_0; + } + + return HTTP_1_1; + } + } + + @Override + @Nullable + protected String serverName(Request request, @Nullable Response response) { + return null; + } + + @Override + @Nullable + protected String clientIp(Request request, @Nullable Response response) { + return null; + } + + @Override + @Nullable + protected Integer statusCode(Request request, Response response) { + return response.getStatus(); + } + + @Override + @Nullable + protected Long responseContentLength(Request request, Response response) { + Long respContentLength = null; + if (response != null) { + HttpField requestContentLengthField = + response.getHeaders().getField(HttpHeader.CONTENT_LENGTH); + respContentLength = getLongFromJettyHttpField(requestContentLengthField); + } + return respContentLength; + } + + @Override + @Nullable + protected Long responseContentLengthUncompressed(Request request, Response response) { + return null; + } + + private static Long getLongFromJettyHttpField(HttpField httpField) { + Long longFromField = null; + try { + longFromField = httpField != null ? Long.getLong(httpField.getValue()) : null; + } catch (NumberFormatException t) { + LOG.debug( + "Value {} is not not valid number format for header field: {}", + httpField.getValue(), + httpField.getName()); + } + return longFromField; + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientInstrumenterBuilder.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientInstrumenterBuilder.java new file mode 100644 index 000000000000..5ca1c4e9531a --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientInstrumenterBuilder.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal; + +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.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.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; + +public final class JettyClientInstrumenterBuilder { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jetty-httpclient-9.2"; + + private final OpenTelemetry openTelemetry; + + private final List> additionalExtractors = + new ArrayList<>(); + + public JettyClientInstrumenterBuilder addAttributeExtractor( + AttributesExtractor attributesExtractor) { + additionalExtractors.add(attributesExtractor); + return this; + } + + public JettyClientInstrumenterBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + public Instrumenter build() { + HttpAttributesExtractor httpAttributesExtractor = + new JettyClientHttpAttributesExtractor(); + SpanNameExtractor spanNameExtractor = + HttpSpanNameExtractor.create(httpAttributesExtractor); + SpanStatusExtractor spanStatusExtractor = + HttpSpanStatusExtractor.create(httpAttributesExtractor); + JettyHttpClientNetAttributesExtractor netAttributesExtractor = + new JettyHttpClientNetAttributesExtractor(); + + Instrumenter instrumenter = + Instrumenter.newBuilder( + this.openTelemetry, INSTRUMENTATION_NAME, spanNameExtractor) + .setSpanStatusExtractor(spanStatusExtractor) + .addAttributesExtractor(httpAttributesExtractor) + .addAttributesExtractor(netAttributesExtractor) + .addAttributesExtractors(additionalExtractors) + .newClientInstrumenter(new HttpHeaderSetter()); + return instrumenter; + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientWrapUtil.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientWrapUtil.java new file mode 100644 index 000000000000..3734b41e0c9f --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientWrapUtil.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal; + +import static java.util.stream.Collectors.toList; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.List; +import org.eclipse.jetty.client.api.Response; + +public final class JettyClientWrapUtil { + + private JettyClientWrapUtil() {} + + /** + * Utility to wrap the response listeners only, this includes the important CompleteListener. + * + * @param parentContext top level context that is above the Jetty client span context + * @param listeners all listeners passed to Jetty client send() method + * @return list of wrapped ResponseListeners + */ + public static List wrapResponseListeners( + Context parentContext, List listeners) { + + return listeners.stream() + .map(listener -> wrapTheListener(listener, parentContext)) + .collect(toList()); + } + + private static Response.ResponseListener wrapTheListener( + Response.ResponseListener listener, Context context) { + Response.ResponseListener wrappedListener = listener; + if (listener instanceof Response.CompleteListener + && !(listener instanceof JettyHttpClient9TracingInterceptor)) { + wrappedListener = + (Response.CompleteListener) + result -> { + try (Scope ignored = context.makeCurrent()) { + ((Response.CompleteListener) listener).onComplete(result); + } + }; + } + return wrappedListener; + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClient9TracingInterceptor.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClient9TracingInterceptor.java new file mode 100644 index 000000000000..b4510048b3aa --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClient9TracingInterceptor.java @@ -0,0 +1,154 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.List; +import java.util.ListIterator; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpHeader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JettyHttpClient9TracingInterceptor does three jobs stimulated from the Jetty Request object from + * attachToRequest() 1. Start the CLIENT span and create the tracer 2. Set the listener callbacks + * for each important lifecycle actions that would cause the start and close of the span 3. Set + * callback wrappers on two important request-based callbacks + */ +public class JettyHttpClient9TracingInterceptor + implements Request.BeginListener, + Request.FailureListener, + Response.SuccessListener, + Response.FailureListener, + Response.CompleteListener { + + private static final Logger LOG = + LoggerFactory.getLogger(JettyHttpClient9TracingInterceptor.class); + + @Nullable private Context context; + + @Nullable + public Context getContext() { + return this.context; + } + + private final Context parentContext; + + private final Instrumenter instrumenter; + + public JettyHttpClient9TracingInterceptor( + Context parentCtx, Instrumenter instrumenter) { + this.parentContext = parentCtx; + this.instrumenter = instrumenter; + } + + public void attachToRequest(Request jettyRequest) { + List current = + jettyRequest.getRequestListeners(JettyHttpClient9TracingInterceptor.class); + + if (!current.isEmpty()) { + LOG.warn("A tracing interceptor is already in place for this request! "); + return; + } + startSpan(jettyRequest); + + // wrap all important request-based listeners that may already be attached, null should ensure + // are returned here + List existingListeners = jettyRequest.getRequestListeners(null); + wrapRequestListeners(existingListeners); + + jettyRequest + .onRequestBegin(this) + .onRequestFailure(this) + .onResponseFailure(this) + .onResponseSuccess(this); + } + + private void wrapRequestListeners(List requestListeners) { + + ListIterator iterator = requestListeners.listIterator(); + while (iterator.hasNext()) { + Request.RequestListener requestListener = iterator.next(); + if (requestListener instanceof Request.FailureListener) { + iterator.set( + (Request.FailureListener) + (request, throwable) -> { + try (Scope ignore = context.makeCurrent()) { + ((Request.FailureListener) requestListener).onFailure(request, throwable); + } + }); + } + if (requestListener instanceof Request.BeginListener) { + iterator.set( + (Request.FailureListener) + (request, throwable) -> { + try (Scope ignore = context.makeCurrent()) { + ((Request.BeginListener) requestListener).onBegin(request); + } + }); + } + } + } + + private void startSpan(Request request) { + + if (!instrumenter.shouldStart(this.parentContext, request)) { + return; + } + this.context = instrumenter.start(this.parentContext, request); + } + + @Override + public void onBegin(Request request) { + if (this.context != null) { + Span span = Span.fromContext(this.context); + HttpField agentField = request.getHeaders().getField(HttpHeader.USER_AGENT); + span.setAttribute(SemanticAttributes.HTTP_USER_AGENT, agentField.getValue()); + } + } + + @Override + public void onComplete(Result result) { + closeIfPossible(result.getResponse()); + } + + @Override + public void onSuccess(Response response) { + closeIfPossible(response); + } + + @Override + public void onFailure(Request request, Throwable t) { + if (this.context != null) { + instrumenter.end(this.context, request, null, t); + } + } + + @Override + public void onFailure(Response response, Throwable t) { + if (this.context != null) { + instrumenter.end(this.context, response.getRequest(), response, t); + } + } + + private void closeIfPossible(Response response) { + + if (this.context != null) { + instrumenter.end(this.context, response.getRequest(), response, null); + } else { + LOG.debug("onComplete - could not find an otel context"); + } + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClientNetAttributesExtractor.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClientNetAttributesExtractor.java new file mode 100644 index 000000000000..1cddd2826b95 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClientNetAttributesExtractor.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; + +public class JettyHttpClientNetAttributesExtractor + extends NetAttributesExtractor { + + @Override + public String transport(Request request) { + return SemanticAttributes.NetTransportValues.IP_TCP; + } + + @Override + @Nullable + public String peerName(Request request, @Nullable Response response) { + return request.getHost(); + } + + @Override + @Nullable + public Integer peerPort(Request request, @Nullable Response response) { + return request.getPort(); + } + + @Override + @Nullable + public String peerIp(Request request, @Nullable Response response) { + // Return null unless the library supports resolution to something similar to SocketAddress + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/3012/files#r633188645 + return null; + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/test/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9LibraryTest.groovy b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/test/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9LibraryTest.groovy new file mode 100644 index 000000000000..091fb9a02a52 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/test/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9LibraryTest.groovy @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v9_2 + + +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import org.eclipse.jetty.client.HttpClient +import org.eclipse.jetty.util.ssl.SslContextFactory + +class JettyHttpClient9LibraryTest extends AbstractJettyClient9Test implements LibraryTestTrait { + + + @Override + boolean testWithClientParent() { + //The client parent test does not work well in the context of library only tests. + false + } + + @Override + HttpClient createStandardClient() { + JettyClientTracingBuilder jettyClientTracingBuilder = new JettyClientTracingBuilder(getOpenTelemetry()) + return jettyClientTracingBuilder.build().getHttpClient() + } + + @Override + HttpClient createHttpsClient(SslContextFactory sslContextFactory) { + JettyClientTracingBuilder jettyClientTracingBuilder = new JettyClientTracingBuilder(getOpenTelemetry()) + return jettyClientTracingBuilder + .setSslContextFactory(sslContextFactory) + .build() + .getHttpClient() + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/build.gradle b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/build.gradle new file mode 100644 index 000000000000..630e8bd91a72 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/build.gradle @@ -0,0 +1,22 @@ +plugins { + id("otel.java-conventions") +} + +//Jetty client 9.2 is the best starting point, HttpClient.send() is stable there +def jettyVers_base9 = '9.2.0.v20140526' + +dependencies { + api(project(':testing-common')) { + exclude group: 'org.eclipse.jetty', module: 'jetty-client' + exclude group: 'org.eclipse.jetty', module: 'jetty-server' + } + + + api "org.eclipse.jetty:jetty-client:${jettyVers_base9}" + + implementation "org.junit.jupiter:junit-jupiter-api" + + implementation "org.codehaus.groovy:groovy-all" + implementation "io.opentelemetry:opentelemetry-api" + implementation "org.spockframework:spock-core" +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/src/main/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/AbstractJettyClient9Test.groovy b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/src/main/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/AbstractJettyClient9Test.groovy new file mode 100644 index 000000000000..7bb236bca3b1 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/src/main/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/AbstractJettyClient9Test.groovy @@ -0,0 +1,150 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v9_2 + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.eclipse.jetty.client.HttpClient +import org.eclipse.jetty.client.api.ContentResponse +import org.eclipse.jetty.client.api.Request +import org.eclipse.jetty.client.api.Response +import org.eclipse.jetty.client.api.Result +import org.eclipse.jetty.http.HttpMethod +import org.eclipse.jetty.util.ssl.SslContextFactory +import org.junit.Rule +import org.junit.rules.TestName +import spock.lang.Shared + +import java.util.concurrent.TimeUnit + +abstract class AbstractJettyClient9Test extends HttpClientTest { + + abstract HttpClient createStandardClient() + + abstract HttpClient createHttpsClient(SslContextFactory sslContextFactory) + + + @Shared + def client = createStandardClient() + @Shared + def httpsClient = null + + @Rule + TestName name = new TestName() + + Request jettyRequest = null + + def setupSpec() { + + //Start the main Jetty HttpClient and a https client + client.start() + + SslContextFactory tlsCtx = new SslContextFactory() + httpsClient = createHttpsClient(tlsCtx) + httpsClient.setFollowRedirects(false) + httpsClient.start() + } + + @Override + Request buildRequest(String method, URI uri, Map headers) { + + HttpClient theClient = uri.scheme == 'https' ? httpsClient : client + + Request request = theClient.newRequest(uri) + + HttpMethod methodObj = HttpMethod.valueOf(method) + request.method(methodObj) + request.timeout(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + + jettyRequest = request + + return request + } + + @Override + String userAgent() { + if (name.methodName.startsWith('connection error') && jettyRequest.getAgent() == null) { + return null + } + return "Jetty" + } + + @Override + int sendRequest(Request request, String method, URI uri, Map headers) { + headers.each { k, v -> + request.header(k, v) + } + + ContentResponse response = request.send() + + return response.status + } + + private static class JettyClientListener implements Request.FailureListener, Response.FailureListener { + + volatile Throwable failure + + @Override + void onFailure(Request requestF, Throwable failure) { + this.failure = failure + + } + + @Override + void onFailure(Response responseF, Throwable failure) { + this.failure = failure + } + + } + + @Override + void sendRequestWithCallback(Request request, String method, URI uri, Map headers, RequestResult requestResult) { + + JettyClientListener jcl = new JettyClientListener() + + request.onRequestFailure(jcl) + request.onResponseFailure(jcl) + headers.each { k, v -> + request.header(k, v) + } + + request.send(new Response.CompleteListener() { + @Override + void onComplete(Result result) { + + if (jcl.failure != null) { + requestResult.complete(jcl.failure) + return + } + + requestResult.complete(result.response.status) + } + }) + } + + + @Override + boolean testRedirects() { + false + } + + @Override + boolean testCausality() { + true + } + + @Override + Set> httpAttributes(URI uri) { + Set> extra = [ + SemanticAttributes.HTTP_SCHEME, + SemanticAttributes.HTTP_TARGET, + SemanticAttributes.HTTP_HOST + ] + super.httpAttributes(uri) + extra + } + +} diff --git a/settings.gradle b/settings.gradle index b3ecfc6d32be..89e78f173d32 100644 --- a/settings.gradle +++ b/settings.gradle @@ -185,6 +185,9 @@ include ':instrumentation:jedis:jedis-3.0:javaagent' include ':instrumentation:jetty:jetty-8.0:javaagent' include ':instrumentation:jetty:jetty-11.0:javaagent' include ':instrumentation:jetty:jetty-common:javaagent' +include ':instrumentation:jetty-httpclient:jetty-httpclient-9.2:javaagent' +include ':instrumentation:jetty-httpclient:jetty-httpclient-9.2:library' +include ':instrumentation:jetty-httpclient:jetty-httpclient-9.2:testing' include ':instrumentation:jms-1.1:javaagent' include ':instrumentation:jms-1.1:javaagent-unit-tests' include ':instrumentation:jsf:jsf-common:library'