diff --git a/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpClientInstrumentation.java b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpClientInstrumentation.java index 5017021409..fe3fd44c85 100644 --- a/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpClientInstrumentation.java +++ b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpClientInstrumentation.java @@ -8,7 +8,7 @@ package io.opentelemetry.javaagent.instrumentation.httpclient; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; -import static io.opentelemetry.javaagent.instrumentation.httpclient.JdkHttpClientTracer.tracer; +import static io.opentelemetry.javaagent.instrumentation.httpclient.JdkHttpClientSingletons.instrumenter; import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; @@ -71,17 +71,18 @@ public class HttpClientInstrumentation implements TypeInstrumentation { @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { Context parentContext = currentContext(); - if (!tracer().shouldStartSpan(parentContext)) { + if (!instrumenter().shouldStart(parentContext, httpRequest)) { return; } - context = tracer().startSpan(parentContext, httpRequest, httpRequest); + context = instrumenter().start(parentContext, httpRequest); scope = context.makeCurrent(); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void methodExit( - @Advice.Return HttpResponse result, + @Advice.Argument(0) HttpRequest httpRequest, + @Advice.Return HttpResponse httpResponse, @Advice.Thrown Throwable throwable, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { @@ -90,11 +91,7 @@ public class HttpClientInstrumentation implements TypeInstrumentation { } scope.close(); - if (throwable == null) { - tracer().end(context, result); - } else { - tracer().endExceptionally(context, result, throwable); - } + instrumenter().end(context, httpRequest, httpResponse, throwable); } } @@ -112,16 +109,17 @@ public class HttpClientInstrumentation implements TypeInstrumentation { if (bodyHandler != null) { bodyHandler = new BodyHandlerWrapper(bodyHandler, parentContext); } - if (!tracer().shouldStartSpan(parentContext)) { + if (!instrumenter().shouldStart(parentContext, httpRequest)) { return; } - context = tracer().startSpan(parentContext, httpRequest, httpRequest); + context = instrumenter().start(parentContext, httpRequest); scope = context.makeCurrent(); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void methodExit( + @Advice.Argument(value = 0) HttpRequest httpRequest, @Advice.Return(readOnly = false) CompletableFuture> future, @Advice.Thrown Throwable throwable, @Advice.Local("otelContext") Context context, @@ -133,9 +131,9 @@ public class HttpClientInstrumentation implements TypeInstrumentation { scope.close(); if (throwable != null) { - tracer().endExceptionally(context, null, throwable); + instrumenter().end(context, httpRequest, null, throwable); } else { - future = future.whenComplete(new ResponseConsumer(context)); + future = future.whenComplete(new ResponseConsumer(context, httpRequest)); future = CompletableFutureWrapper.wrap(future, parentContext); } } diff --git a/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInjectAdapter.java b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInjectAdapter.java index 801f02ad5e..28cd2e65db 100644 --- a/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInjectAdapter.java +++ b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInjectAdapter.java @@ -5,15 +5,40 @@ package io.opentelemetry.javaagent.instrumentation.httpclient; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.context.propagation.TextMapSetter; +import java.net.http.HttpHeaders; import java.net.http.HttpRequest; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; -/** Context propagation is implemented via {@link HttpHeadersInstrumentation}. */ +/** Context propagation is initiated via {@link HttpHeadersInstrumentation}. */ public class HttpHeadersInjectAdapter implements TextMapSetter { - public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter(); + + private final ContextPropagators contextPropagators; + + public HttpHeadersInjectAdapter(ContextPropagators contextPropagators) { + this.contextPropagators = contextPropagators; + } @Override public void set(HttpRequest carrier, String key, String value) { // Don't do anything because headers are immutable } + + public HttpHeaders inject(HttpHeaders original) { + Map> headerMap = new HashMap<>(original.map()); + + contextPropagators + .getTextMapPropagator() + .inject( + Context.current(), + headerMap, + (carrier, key, value) -> carrier.put(key, Collections.singletonList(value))); + + return HttpHeaders.of(headerMap, (s, s2) -> true); + } } diff --git a/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInstrumentation.java b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInstrumentation.java index 3505775169..71d6cac80c 100644 --- a/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInstrumentation.java +++ b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInstrumentation.java @@ -6,7 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.httpclient; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; -import static io.opentelemetry.javaagent.instrumentation.httpclient.JdkHttpClientTracer.tracer; +import static io.opentelemetry.javaagent.instrumentation.httpclient.JdkHttpClientSingletons.setter; import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -39,7 +39,7 @@ public class HttpHeadersInstrumentation implements TypeInstrumentation { @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void methodExit(@Advice.Return(readOnly = false) HttpHeaders headers) { - headers = tracer().inject(headers); + headers = setter().inject(headers); } } } diff --git a/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpAttributesExtractor.java b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpAttributesExtractor.java new file mode 100644 index 0000000000..4cb251895d --- /dev/null +++ b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpAttributesExtractor.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpclient; + +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import org.checkerframework.checker.nullness.qual.Nullable; + +class JdkHttpAttributesExtractor extends HttpAttributesExtractor> { + + @Override + protected String method(HttpRequest httpRequest) { + return httpRequest.method(); + } + + @Override + protected String url(HttpRequest httpRequest) { + return httpRequest.uri().toString(); + } + + @Override + protected @Nullable String target(HttpRequest httpRequest) { + return null; + } + + @Override + protected @Nullable String host(HttpRequest httpRequest) { + return httpRequest.uri().getHost(); + } + + @Override + protected @Nullable String scheme(HttpRequest httpRequest) { + return httpRequest.uri().getScheme(); + } + + @Override + protected @Nullable String userAgent(HttpRequest httpRequest) { + return httpRequest.headers().firstValue("User-Agent").orElse(null); + } + + @Override + protected @Nullable Long requestContentLength( + HttpRequest httpRequest, @Nullable HttpResponse httpResponse) { + return null; + } + + @Override + protected @Nullable Long requestContentLengthUncompressed( + HttpRequest httpRequest, @Nullable HttpResponse httpResponse) { + return null; + } + + @Override + protected Integer statusCode(HttpRequest httpRequest, HttpResponse httpResponse) { + return httpResponse.statusCode(); + } + + @Override + protected String flavor(HttpRequest httpRequest, @Nullable HttpResponse httpResponse) { + if (httpResponse != null && httpResponse.version() == Version.HTTP_2) { + return SemanticAttributes.HttpFlavorValues.HTTP_2_0; + } + return SemanticAttributes.HttpFlavorValues.HTTP_1_1; + } + + @Override + protected @Nullable Long responseContentLength( + HttpRequest httpRequest, HttpResponse httpResponse) { + return null; + } + + @Override + protected @Nullable Long responseContentLengthUncompressed( + HttpRequest httpRequest, HttpResponse httpResponse) { + return null; + } + + @Override + protected @Nullable String serverName( + HttpRequest httpRequest, @Nullable HttpResponse httpResponse) { + return null; + } + + @Override + protected @Nullable String route(HttpRequest httpRequest) { + return null; + } +} diff --git a/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpClientSingletons.java b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpClientSingletons.java new file mode 100644 index 0000000000..3af0c0d2b5 --- /dev/null +++ b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpClientSingletons.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpclient; + +import io.opentelemetry.api.GlobalOpenTelemetry; +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.HttpClientMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; +import io.opentelemetry.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public class JdkHttpClientSingletons { + + private static final HttpHeadersInjectAdapter SETTER; + private static final Instrumenter> INSTRUMENTER; + + static { + SETTER = new HttpHeadersInjectAdapter(GlobalOpenTelemetry.getPropagators()); + JdkHttpAttributesExtractor httpAttributesExtractor = new JdkHttpAttributesExtractor(); + SpanNameExtractor spanNameExtractor = + HttpSpanNameExtractor.create(httpAttributesExtractor); + SpanStatusExtractor> spanStatusExtractor = + HttpSpanStatusExtractor.create(httpAttributesExtractor); + JdkHttpNetAttributesExtractor netAttributesExtractor = new JdkHttpNetAttributesExtractor(); + + INSTRUMENTER = + Instrumenter.>newBuilder( + GlobalOpenTelemetry.get(), "io.opentelemetry.java-http-client", spanNameExtractor) + .setSpanStatusExtractor(spanStatusExtractor) + .addAttributesExtractor(httpAttributesExtractor) + .addAttributesExtractor(netAttributesExtractor) + .addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor)) + .addRequestMetrics(HttpClientMetrics.get()) + .newClientInstrumenter(SETTER); + } + + public static Instrumenter> instrumenter() { + return INSTRUMENTER; + } + + public static HttpHeadersInjectAdapter setter() { + return SETTER; + } + + private JdkHttpClientSingletons() {} +} diff --git a/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpClientTracer.java b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpClientTracer.java deleted file mode 100644 index 374c98fc6f..0000000000 --- a/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpClientTracer.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.httpclient; - -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.propagation.TextMapSetter; -import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; -import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.net.URI; -import java.net.http.HttpClient.Version; -import java.net.http.HttpHeaders; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class JdkHttpClientTracer - extends HttpClientTracer> { - private static final JdkHttpClientTracer TRACER = new JdkHttpClientTracer(); - - private JdkHttpClientTracer() { - super(NetPeerAttributes.INSTANCE); - } - - public static JdkHttpClientTracer tracer() { - return TRACER; - } - - @Override - protected String getInstrumentationName() { - return "io.opentelemetry.java-http-client"; - } - - @Override - protected String method(HttpRequest httpRequest) { - return httpRequest.method(); - } - - @Override - protected URI url(HttpRequest httpRequest) { - return httpRequest.uri(); - } - - @Override - protected Integer status(HttpResponse httpResponse) { - return httpResponse.statusCode(); - } - - @Override - protected String requestHeader(HttpRequest httpRequest, String name) { - return httpRequest.headers().firstValue(name).orElse(null); - } - - @Override - protected String responseHeader(HttpResponse httpResponse, String name) { - return httpResponse.headers().firstValue(name).orElse(null); - } - - @Override - protected void onResponse(Span span, HttpResponse httpResponse) { - super.onResponse(span, httpResponse); - - if (httpResponse != null) { - span.setAttribute( - SemanticAttributes.HTTP_FLAVOR, - httpResponse.version() == Version.HTTP_1_1 ? "1.1" : "2.0"); - } - } - - @Override - protected TextMapSetter getSetter() { - return HttpHeadersInjectAdapter.SETTER; - } - - public HttpHeaders inject(HttpHeaders original) { - Map> headerMap = new HashMap<>(original.map()); - - inject( - Context.current(), - headerMap, - (carrier, key, value) -> carrier.put(key, Collections.singletonList(value))); - - return HttpHeaders.of(headerMap, (s, s2) -> true); - } -} diff --git a/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpNetAttributesExtractor.java b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpNetAttributesExtractor.java new file mode 100644 index 0000000000..5d78c26479 --- /dev/null +++ b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpNetAttributesExtractor.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpclient; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JdkHttpNetAttributesExtractor + extends NetAttributesExtractor> { + + private static final Logger logger = LoggerFactory.getLogger(JdkHttpNetAttributesExtractor.class); + + @Override + public String transport(HttpRequest httpRequest) { + return SemanticAttributes.NetTransportValues.IP_TCP; + } + + @Override + public @Nullable String peerName( + HttpRequest httpRequest, @Nullable HttpResponse httpResponse) { + return httpRequest.uri().getHost(); + } + + @Override + public @Nullable Integer peerPort( + HttpRequest httpRequest, @Nullable HttpResponse httpResponse) { + int port = httpRequest.uri().getPort(); + if (port != -1) { + return port; + } + String scheme = httpRequest.uri().getScheme(); + if (scheme == null) { + return 80; + } + switch (scheme) { + case "http": + return 80; + case "https": + return 443; + default: + logger.debug("no default port mapping for scheme: {}", scheme); + return null; + } + } + + @Override + public @Nullable String peerIp(HttpRequest httpRequest, @Nullable HttpResponse httpResponse) { + return null; + } +} diff --git a/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/ResponseConsumer.java b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/ResponseConsumer.java index fce963a6d7..c04b4c59ff 100644 --- a/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/ResponseConsumer.java +++ b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/ResponseConsumer.java @@ -5,25 +5,24 @@ package io.opentelemetry.javaagent.instrumentation.httpclient; -import static io.opentelemetry.javaagent.instrumentation.httpclient.JdkHttpClientTracer.tracer; +import static io.opentelemetry.javaagent.instrumentation.httpclient.JdkHttpClientSingletons.instrumenter; import io.opentelemetry.context.Context; +import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.function.BiConsumer; public class ResponseConsumer implements BiConsumer, Throwable> { private final Context context; + private final HttpRequest httpRequest; - public ResponseConsumer(Context context) { + public ResponseConsumer(Context context, HttpRequest httpRequest) { this.context = context; + this.httpRequest = httpRequest; } @Override public void accept(HttpResponse httpResponse, Throwable throwable) { - if (throwable == null) { - tracer().end(context, httpResponse); - } else { - tracer().endExceptionally(context, httpResponse, throwable); - } + instrumenter().end(context, httpRequest, httpResponse, throwable); } }