diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpAsyncClientInstrumentation.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpAsyncClientInstrumentation.java index 5a6885710e0e..1bfcfe404356 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpAsyncClientInstrumentation.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpAsyncClientInstrumentation.java @@ -8,6 +8,7 @@ import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0.ApacheHttpClientSingletons.createOrGetBytesTransferMetrics; import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0.ApacheHttpClientSingletons.instrumenter; import static java.util.logging.Level.FINE; import static net.bytebuddy.matcher.ElementMatchers.isMethod; @@ -20,6 +21,8 @@ import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; import java.util.logging.Logger; import javax.annotation.Nullable; import net.bytebuddy.asm.Advice; @@ -27,10 +30,13 @@ import net.bytebuddy.matcher.ElementMatcher; import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.nio.AsyncRequestProducer; +import org.apache.hc.core5.http.nio.AsyncResponseConsumer; +import org.apache.hc.core5.http.nio.CapacityChannel; import org.apache.hc.core5.http.nio.DataStreamChannel; import org.apache.hc.core5.http.nio.RequestChannel; import org.apache.hc.core5.http.protocol.BasicHttpContext; @@ -69,6 +75,7 @@ public static class ClientAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void methodEnter( @Advice.Argument(value = 0, readOnly = false) AsyncRequestProducer requestProducer, + @Advice.Argument(value = 1, readOnly = false) AsyncResponseConsumer responseConsumer, @Advice.Argument(value = 3, readOnly = false) HttpContext httpContext, @Advice.Argument(value = 4, readOnly = false) FutureCallback futureCallback) { @@ -80,23 +87,81 @@ public static void methodEnter( WrappedFutureCallback wrappedFutureCallback = new WrappedFutureCallback<>(parentContext, httpContext, futureCallback); requestProducer = - new DelegatingRequestProducer(parentContext, requestProducer, wrappedFutureCallback); + new WrappedRequestProducer(parentContext, requestProducer, wrappedFutureCallback); + responseConsumer = new WrappedResponseConsumer<>(parentContext, responseConsumer); futureCallback = wrappedFutureCallback; } } - public static class DelegatingRequestProducer implements AsyncRequestProducer { + public static class WrappedResponseConsumer implements AsyncResponseConsumer { + private final AsyncResponseConsumer delegate; + private final Context parentContext; + + public WrappedResponseConsumer(Context parentContext, AsyncResponseConsumer delegate) { + this.parentContext = parentContext; + this.delegate = delegate; + } + + @Override + public void consumeResponse( + HttpResponse httpResponse, + EntityDetails entityDetails, + HttpContext httpContext, + FutureCallback futureCallback) + throws HttpException, IOException { + if (entityDetails != null) { + BytesTransferMetrics metrics = createOrGetBytesTransferMetrics(parentContext); + metrics.setResponseContentLength(entityDetails.getContentLength()); + } + delegate.consumeResponse(httpResponse, entityDetails, httpContext, futureCallback); + } + + @Override + public void informationResponse(HttpResponse httpResponse, HttpContext httpContext) + throws HttpException, IOException { + delegate.informationResponse(httpResponse, httpContext); + } + + @Override + public void failed(Exception e) { + delegate.failed(e); + } + + @Override + public void updateCapacity(CapacityChannel capacityChannel) throws IOException { + delegate.updateCapacity(capacityChannel); + } + + @Override + public void consume(ByteBuffer byteBuffer) throws IOException { + if (byteBuffer.hasRemaining()) { + BytesTransferMetrics metrics = createOrGetBytesTransferMetrics(parentContext); + metrics.addResponseBytes(byteBuffer.limit()); + } + delegate.consume(byteBuffer); + } + + @Override + public void streamEnd(List list) throws HttpException, IOException { + delegate.streamEnd(list); + } + + @Override + public void releaseResources() { + delegate.releaseResources(); + } + } + + public static class WrappedRequestProducer implements AsyncRequestProducer { private final Context parentContext; private final AsyncRequestProducer delegate; - private final WrappedFutureCallback wrappedFutureCallback; + private final WrappedFutureCallback callback; - public DelegatingRequestProducer( - Context parentContext, - AsyncRequestProducer delegate, - WrappedFutureCallback wrappedFutureCallback) { + public WrappedRequestProducer( + Context parentContext, AsyncRequestProducer delegate, WrappedFutureCallback callback) { this.parentContext = parentContext; this.delegate = delegate; - this.wrappedFutureCallback = wrappedFutureCallback; + this.callback = callback; } @Override @@ -107,8 +172,7 @@ public void failed(Exception ex) { @Override public void sendRequest(RequestChannel channel, HttpContext context) throws HttpException, IOException { - DelegatingRequestChannel requestChannel = - new DelegatingRequestChannel(channel, parentContext, wrappedFutureCallback); + RequestChannel requestChannel = new WrappedRequestChannel(channel, parentContext, callback); delegate.sendRequest(requestChannel, context); } @@ -124,7 +188,7 @@ public int available() { @Override public void produce(DataStreamChannel channel) throws IOException { - delegate.produce(channel); + delegate.produce(new WrappedDataStreamChannel(parentContext, channel)); } @Override @@ -133,12 +197,44 @@ public void releaseResources() { } } - public static class DelegatingRequestChannel implements RequestChannel { + public static class WrappedDataStreamChannel implements DataStreamChannel { + private final Context parentContext; + private final DataStreamChannel delegate; + + public WrappedDataStreamChannel(Context parentContext, DataStreamChannel delegate) { + this.parentContext = parentContext; + this.delegate = delegate; + } + + @Override + public void requestOutput() { + delegate.requestOutput(); + } + + @Override + public int write(ByteBuffer byteBuffer) throws IOException { + BytesTransferMetrics metrics = createOrGetBytesTransferMetrics(parentContext); + metrics.addRequestBytes(byteBuffer.limit()); + return delegate.write(byteBuffer); + } + + @Override + public void endStream() throws IOException { + delegate.endStream(); + } + + @Override + public void endStream(List list) throws IOException { + delegate.endStream(list); + } + } + + public static class WrappedRequestChannel implements RequestChannel { private final RequestChannel delegate; private final Context parentContext; private final WrappedFutureCallback wrappedFutureCallback; - public DelegatingRequestChannel( + public WrappedRequestChannel( RequestChannel requestChannel, Context parentContext, WrappedFutureCallback wrappedFutureCallback) { @@ -150,9 +246,14 @@ public DelegatingRequestChannel( @Override public void sendRequest(HttpRequest request, EntityDetails entityDetails, HttpContext context) throws HttpException, IOException { - if (instrumenter().shouldStart(parentContext, request)) { - wrappedFutureCallback.context = instrumenter().start(parentContext, request); - wrappedFutureCallback.httpRequest = request; + if (entityDetails != null) { + BytesTransferMetrics metrics = createOrGetBytesTransferMetrics(parentContext); + metrics.setRequestContentLength(entityDetails.getContentLength()); + } + ApacheHttpClientRequest otelRequest = new ApacheHttpClientRequest(parentContext, request); + if (instrumenter().shouldStart(parentContext, otelRequest)) { + wrappedFutureCallback.context = instrumenter().start(parentContext, otelRequest); + wrappedFutureCallback.otelRequest = otelRequest; } delegate.sendRequest(request, entityDetails, context); @@ -160,20 +261,19 @@ public void sendRequest(HttpRequest request, EntityDetails entityDetails, HttpCo } public static class WrappedFutureCallback implements FutureCallback { - private static final Logger logger = Logger.getLogger(WrappedFutureCallback.class.getName()); private final Context parentContext; - private final HttpContext httpContext; + private final HttpCoreContext httpContext; private final FutureCallback delegate; private volatile Context context; - private volatile HttpRequest httpRequest; + private volatile ApacheHttpClientRequest otelRequest; public WrappedFutureCallback( Context parentContext, HttpContext httpContext, FutureCallback delegate) { this.parentContext = parentContext; - this.httpContext = httpContext; + this.httpContext = HttpCoreContext.adapt(httpContext); // Note: this can be null in real life, so we have to handle this carefully this.delegate = delegate; } @@ -187,7 +287,7 @@ public void completed(T result) { return; } - instrumenter().end(context, httpRequest, getResponseFromHttpContext(), null); + instrumenter().end(context, otelRequest, getResponse(), null); if (parentContext == null) { completeDelegate(result); @@ -209,7 +309,7 @@ public void failed(Exception ex) { } // end span before calling delegate - instrumenter().end(context, httpRequest, getResponseFromHttpContext(), ex); + instrumenter().end(context, otelRequest, getResponse(), ex); if (parentContext == null) { failDelegate(ex); @@ -232,7 +332,7 @@ public void cancelled() { // TODO (trask) add "canceled" span attribute // end span before calling delegate - instrumenter().end(context, httpRequest, getResponseFromHttpContext(), null); + instrumenter().end(context, otelRequest, getResponse(), null); if (parentContext == null) { cancelDelegate(); @@ -263,8 +363,8 @@ private void cancelDelegate() { } @Nullable - private HttpResponse getResponseFromHttpContext() { - return (HttpResponse) httpContext.getAttribute(HttpCoreContext.HTTP_RESPONSE); + private HttpResponse getResponse() { + return httpContext.getResponse(); } } } diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientContentLengthAttributesGetter.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientContentLengthAttributesGetter.java new file mode 100644 index 000000000000..1153bd309347 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientContentLengthAttributesGetter.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0.ApacheHttpClientSingletons.getBytesTransferMetrics; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.apache.hc.core5.http.HttpResponse; + +public class ApacheHttpClientContentLengthAttributesGetter + implements AttributesExtractor { + + @Override + public void onStart( + AttributesBuilder attributes, Context parentContext, ApacheHttpClientRequest request) {} + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + ApacheHttpClientRequest request, + HttpResponse response, + Throwable error) { + Context parentContext = request.getParentContext(); + BytesTransferMetrics metrics = getBytesTransferMetrics(parentContext); + if (metrics != null) { + Long responseLength = metrics.getResponseContentLength(); + if (responseLength != null) { + attributes.put(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, responseLength); + } + Long requestLength = metrics.getRequestContentLength(); + if (requestLength != null) { + attributes.put(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, requestLength); + } + } + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHelper.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHelper.java index 0a75742dd513..dffdf7b07b16 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHelper.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHelper.java @@ -5,24 +5,93 @@ package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; +import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0.ApacheHttpClientSingletons.createOrGetBytesTransferMetrics; import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0.ApacheHttpClientSingletons.instrumenter; import io.opentelemetry.context.Context; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.MessageHeaders; +import org.apache.hc.core5.http.ProtocolVersion; public class ApacheHttpClientHelper { + private static final Logger logger = Logger.getLogger(ApacheHttpClientHelper.class.getName()); + + public static void doOnMethodEnter(Context parentContext, ClassicHttpRequest request) { + HttpEntity originalEntity = request.getEntity(); + if (originalEntity != null) { + HttpEntity wrappedHttpEntity = new WrappedHttpEntity(parentContext, originalEntity); + request.setEntity(wrappedHttpEntity); + } + } public static void doMethodExit( - Context context, ClassicHttpRequest request, Object result, Throwable throwable) { + Context context, ApacheHttpClientRequest otelRequest, Object result, Throwable throwable) { + if (result instanceof CloseableHttpResponse) { + HttpEntity entity = ((CloseableHttpResponse) result).getEntity(); + if (entity != null) { + long contentLength = entity.getContentLength(); + Context parentContext = otelRequest.getParentContext(); + BytesTransferMetrics metrics = createOrGetBytesTransferMetrics(parentContext); + metrics.setResponseContentLength(contentLength); + } + } if (throwable != null) { - instrumenter().end(context, request, null, throwable); + instrumenter().end(context, otelRequest, null, throwable); } else if (result instanceof HttpResponse) { - instrumenter().end(context, request, (HttpResponse) result, null); + instrumenter().end(context, otelRequest, (HttpResponse) result, null); } else { // ended in WrappingStatusSettingResponseHandler } } + public static List getHeader(MessageHeaders messageHeaders, String name) { + return headersToList(messageHeaders.getHeaders(name)); + } + + // minimize memory overhead by not using streams + private static List headersToList(Header[] headers) { + if (headers.length == 0) { + return Collections.emptyList(); + } + List headersList = new ArrayList<>(headers.length); + for (Header header : headers) { + headersList.add(header.getValue()); + } + return headersList; + } + + public static String getFlavor(ProtocolVersion protocolVersion) { + if (protocolVersion == null) { + return null; + } + 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.log(Level.FINE, "unexpected http protocol version: {0}", protocolVersion); + return null; + } + private ApacheHttpClientHelper() {} } diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHttpAttributesGetter.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHttpAttributesGetter.java index a1f229763493..310258a7124f 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHttpAttributesGetter.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHttpAttributesGetter.java @@ -6,125 +6,47 @@ package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.annotation.Nullable; -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; -import org.apache.hc.core5.http.MessageHeaders; -import org.apache.hc.core5.http.ProtocolVersion; -import org.apache.hc.core5.net.URIAuthority; final class ApacheHttpClientHttpAttributesGetter - implements HttpClientAttributesGetter { - private static final Logger logger = - Logger.getLogger(ApacheHttpClientHttpAttributesGetter.class.getName()); + implements HttpClientAttributesGetter { @Override - public String method(HttpRequest request) { + public String method(ApacheHttpClientRequest request) { return request.getMethod(); } @Override - public String url(HttpRequest request) { - // similar to org.apache.hc.core5.http.message.BasicHttpRequest.getUri() - // not calling getUri() to avoid unnecessary conversion - StringBuilder url = new StringBuilder(); - URIAuthority authority = request.getAuthority(); - if (authority != null) { - String scheme = request.getScheme(); - if (scheme != null) { - url.append(scheme); - url.append("://"); - } else { - url.append("http://"); - } - url.append(authority.getHostName()); - int port = authority.getPort(); - if (port >= 0) { - url.append(":"); - url.append(port); - } - } - String path = request.getPath(); - if (path != null) { - if (url.length() > 0 && !path.startsWith("/")) { - url.append("/"); - } - url.append(path); - } else { - url.append("/"); - } - return url.toString(); + public String url(ApacheHttpClientRequest request) { + return request.getUrl(); } @Override - public List requestHeader(HttpRequest request, String name) { - return getHeader(request, name); + public List requestHeader(ApacheHttpClientRequest request, String name) { + return request.getHeader(name); } @Override - public Integer statusCode(HttpRequest request, HttpResponse response, @Nullable Throwable error) { + public Integer statusCode( + ApacheHttpClientRequest request, HttpResponse response, @Nullable Throwable error) { return response.getCode(); } @Override @Nullable - public String flavor(HttpRequest request, @Nullable HttpResponse response) { - ProtocolVersion protocolVersion = getVersion(request, response); - if (protocolVersion == null) { - return null; - } - 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; + public String flavor(ApacheHttpClientRequest request, @Nullable HttpResponse response) { + String flavor = request.getFlavor(); + if (flavor == null && response != null) { + flavor = ApacheHttpClientHelper.getFlavor(response.getVersion()); } - if (major == 1 && minor == 1) { - return SemanticAttributes.HttpFlavorValues.HTTP_1_1; - } - if (major == 2 && minor == 0) { - return SemanticAttributes.HttpFlavorValues.HTTP_2_0; - } - logger.log(Level.FINE, "unexpected http protocol version: {0}", protocolVersion); - return null; + return flavor; } @Override - public List responseHeader(HttpRequest request, HttpResponse response, String name) { - return getHeader(response, name); - } - - private static ProtocolVersion getVersion(HttpRequest request, @Nullable HttpResponse response) { - ProtocolVersion protocolVersion = request.getVersion(); - if (protocolVersion == null && response != null) { - protocolVersion = response.getVersion(); - } - return protocolVersion; - } - - private static List getHeader(MessageHeaders messageHeaders, String name) { - return headersToList(messageHeaders.getHeaders(name)); - } - - // minimize memory overhead by not using streams - private static List headersToList(Header[] headers) { - if (headers.length == 0) { - return Collections.emptyList(); - } - List headersList = new ArrayList<>(headers.length); - for (Header header : headers) { - headersList.add(header.getValue()); - } - return headersList; + public List responseHeader( + ApacheHttpClientRequest request, HttpResponse response, String name) { + return ApacheHttpClientHelper.getHeader(response, name); } } diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientInstrumentation.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientInstrumentation.java index b5897ec9ecc6..cee842279bd0 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientInstrumentation.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientInstrumentation.java @@ -127,22 +127,27 @@ public static class RequestAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void methodEnter( @Advice.Argument(0) ClassicHttpRequest request, + @Advice.Local("otelRequest") ApacheHttpClientRequest otelRequest, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { Context parentContext = currentContext(); - if (!instrumenter().shouldStart(parentContext, request)) { + + otelRequest = new ApacheHttpClientRequest(parentContext, request); + + if (!instrumenter().shouldStart(parentContext, otelRequest)) { return; } - context = instrumenter().start(parentContext, request); + ApacheHttpClientHelper.doOnMethodEnter(parentContext, request); + context = instrumenter().start(parentContext, otelRequest); scope = context.makeCurrent(); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void methodExit( - @Advice.Argument(0) ClassicHttpRequest request, @Advice.Return Object result, @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") ApacheHttpClientRequest otelRequest, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { if (scope == null) { @@ -150,7 +155,7 @@ public static void methodExit( } scope.close(); - ApacheHttpClientHelper.doMethodExit(context, request, result, throwable); + ApacheHttpClientHelper.doMethodExit(context, otelRequest, result, throwable); } } @@ -161,28 +166,34 @@ public static class RequestWithHandlerAdvice { public static void methodEnter( @Advice.Argument(0) ClassicHttpRequest request, @Advice.Argument(value = 1, readOnly = false) HttpClientResponseHandler handler, + @Advice.Local("otelRequest") ApacheHttpClientRequest otelRequest, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { Context parentContext = currentContext(); - if (!instrumenter().shouldStart(parentContext, request)) { + + otelRequest = new ApacheHttpClientRequest(parentContext, request); + + if (!instrumenter().shouldStart(parentContext, otelRequest)) { return; } - context = instrumenter().start(parentContext, request); + ApacheHttpClientHelper.doOnMethodEnter(parentContext, request); + context = instrumenter().start(parentContext, otelRequest); scope = context.makeCurrent(); // Wrap the handler so we capture the status code if (handler != null) { handler = - new WrappingStatusSettingResponseHandler<>(context, parentContext, request, handler); + new WrappingStatusSettingResponseHandler<>( + context, parentContext, otelRequest, handler); } } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void methodExit( - @Advice.Argument(0) ClassicHttpRequest request, @Advice.Return Object result, @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") ApacheHttpClientRequest otelRequest, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { if (scope == null) { @@ -190,7 +201,7 @@ public static void methodExit( } scope.close(); - ApacheHttpClientHelper.doMethodExit(context, request, result, throwable); + ApacheHttpClientHelper.doMethodExit(context, otelRequest, result, throwable); } } @@ -201,28 +212,34 @@ public static class RequestWithContextAndHandlerAdvice { public static void methodEnter( @Advice.Argument(0) ClassicHttpRequest request, @Advice.Argument(value = 2, readOnly = false) HttpClientResponseHandler handler, + @Advice.Local("otelRequest") ApacheHttpClientRequest otelRequest, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { Context parentContext = currentContext(); - if (!instrumenter().shouldStart(parentContext, request)) { + + otelRequest = new ApacheHttpClientRequest(parentContext, request); + + if (!instrumenter().shouldStart(parentContext, otelRequest)) { return; } - context = instrumenter().start(parentContext, request); + ApacheHttpClientHelper.doOnMethodEnter(parentContext, request); + context = instrumenter().start(parentContext, otelRequest); scope = context.makeCurrent(); // Wrap the handler so we capture the status code if (handler != null) { handler = - new WrappingStatusSettingResponseHandler<>(context, parentContext, request, handler); + new WrappingStatusSettingResponseHandler<>( + context, parentContext, otelRequest, handler); } } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void methodExit( - @Advice.Argument(0) ClassicHttpRequest request, @Advice.Return Object result, @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") ApacheHttpClientRequest otelRequest, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { if (scope == null) { @@ -230,7 +247,7 @@ public static void methodExit( } scope.close(); - ApacheHttpClientHelper.doMethodExit(context, request, result, throwable); + ApacheHttpClientHelper.doMethodExit(context, otelRequest, result, throwable); } } @@ -241,16 +258,19 @@ public static class RequestWithHostAdvice { public static void methodEnter( @Advice.Argument(0) HttpHost host, @Advice.Argument(1) ClassicHttpRequest request, - @Advice.Local("otelFullRequest") ClassicHttpRequest fullRequest, + @Advice.Local("otelRequest") ApacheHttpClientRequest otelRequest, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { Context parentContext = currentContext(); - fullRequest = new RequestWithHost(host, request); - if (!instrumenter().shouldStart(parentContext, fullRequest)) { + + otelRequest = new ApacheHttpClientRequest(parentContext, new RequestWithHost(host, request)); + + if (!instrumenter().shouldStart(parentContext, otelRequest)) { return; } - context = instrumenter().start(parentContext, fullRequest); + ApacheHttpClientHelper.doOnMethodEnter(parentContext, request); + context = instrumenter().start(parentContext, otelRequest); scope = context.makeCurrent(); } @@ -258,7 +278,7 @@ public static void methodEnter( public static void methodExit( @Advice.Return Object result, @Advice.Thrown Throwable throwable, - @Advice.Local("otelFullRequest") ClassicHttpRequest fullRequest, + @Advice.Local("otelRequest") ApacheHttpClientRequest otelRequest, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { if (scope == null) { @@ -266,7 +286,7 @@ public static void methodExit( } scope.close(); - ApacheHttpClientHelper.doMethodExit(context, fullRequest, result, throwable); + ApacheHttpClientHelper.doMethodExit(context, otelRequest, result, throwable); } } @@ -278,24 +298,26 @@ public static void methodEnter( @Advice.Argument(0) HttpHost host, @Advice.Argument(1) ClassicHttpRequest request, @Advice.Argument(value = 2, readOnly = false) HttpClientResponseHandler handler, - @Advice.Local("otelFullRequest") ClassicHttpRequest fullRequest, + @Advice.Local("otelRequest") ApacheHttpClientRequest otelRequest, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { - Context parentContext = currentContext(); - fullRequest = new RequestWithHost(host, request); - if (!instrumenter().shouldStart(parentContext, fullRequest)) { + + otelRequest = new ApacheHttpClientRequest(parentContext, new RequestWithHost(host, request)); + + if (!instrumenter().shouldStart(parentContext, otelRequest)) { return; } - context = instrumenter().start(parentContext, fullRequest); + ApacheHttpClientHelper.doOnMethodEnter(parentContext, request); + context = instrumenter().start(parentContext, otelRequest); scope = context.makeCurrent(); // Wrap the handler so we capture the status code if (handler != null) { handler = new WrappingStatusSettingResponseHandler<>( - context, parentContext, fullRequest, handler); + context, parentContext, otelRequest, handler); } } @@ -303,7 +325,7 @@ public static void methodEnter( public static void methodExit( @Advice.Return Object result, @Advice.Thrown Throwable throwable, - @Advice.Local("otelFullRequest") ClassicHttpRequest fullRequest, + @Advice.Local("otelRequest") ApacheHttpClientRequest otelRequest, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { if (scope == null) { @@ -311,7 +333,7 @@ public static void methodExit( } scope.close(); - ApacheHttpClientHelper.doMethodExit(context, fullRequest, result, throwable); + ApacheHttpClientHelper.doMethodExit(context, otelRequest, result, throwable); } } @@ -323,24 +345,26 @@ public static void methodEnter( @Advice.Argument(0) HttpHost host, @Advice.Argument(1) ClassicHttpRequest request, @Advice.Argument(value = 3, readOnly = false) HttpClientResponseHandler handler, - @Advice.Local("otelFullRequest") ClassicHttpRequest fullRequest, + @Advice.Local("otelRequest") ApacheHttpClientRequest otelRequest, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { - Context parentContext = currentContext(); - fullRequest = new RequestWithHost(host, request); - if (!instrumenter().shouldStart(parentContext, fullRequest)) { + + otelRequest = new ApacheHttpClientRequest(parentContext, new RequestWithHost(host, request)); + + if (!instrumenter().shouldStart(parentContext, otelRequest)) { return; } - context = instrumenter().start(parentContext, fullRequest); + ApacheHttpClientHelper.doOnMethodEnter(parentContext, request); + context = instrumenter().start(parentContext, otelRequest); scope = context.makeCurrent(); // Wrap the handler so we capture the status code if (handler != null) { handler = new WrappingStatusSettingResponseHandler<>( - context, parentContext, fullRequest, handler); + context, parentContext, otelRequest, handler); } } @@ -348,7 +372,7 @@ public static void methodEnter( public static void methodExit( @Advice.Return Object result, @Advice.Thrown Throwable throwable, - @Advice.Local("otelFullRequest") ClassicHttpRequest fullRequest, + @Advice.Local("otelRequest") ApacheHttpClientRequest otelRequest, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { if (scope == null) { @@ -356,7 +380,7 @@ public static void methodExit( } scope.close(); - ApacheHttpClientHelper.doMethodExit(context, fullRequest, result, throwable); + ApacheHttpClientHelper.doMethodExit(context, otelRequest, result, throwable); } } } diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientNetAttributesGetter.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientNetAttributesGetter.java index d2829dc68097..c1c56698666a 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientNetAttributesGetter.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientNetAttributesGetter.java @@ -5,49 +5,26 @@ package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; -import static java.util.logging.Level.FINE; - import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.logging.Logger; import javax.annotation.Nullable; -import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; final class ApacheHttpClientNetAttributesGetter - implements NetClientAttributesGetter { - private static final Logger logger = - Logger.getLogger(ApacheHttpClientNetAttributesGetter.class.getName()); - + implements NetClientAttributesGetter { @Override - public String transport(HttpRequest request, @Nullable HttpResponse response) { + public String transport(ApacheHttpClientRequest request, @Nullable HttpResponse response) { return SemanticAttributes.NetTransportValues.IP_TCP; } @Override @Nullable - public String peerName(HttpRequest request) { - return request.getAuthority().getHostName(); + public String peerName(ApacheHttpClientRequest request) { + return request.getPeerName(); } @Override - public Integer peerPort(HttpRequest request) { - int port = request.getAuthority().getPort(); - if (port != -1) { - return port; - } - String scheme = request.getScheme(); - if (scheme == null) { - return 80; - } - switch (scheme) { - case "http": - return 80; - case "https": - return 443; - default: - logger.log(FINE, "no default port mapping for scheme: {0}", scheme); - return null; - } + public Integer peerPort(ApacheHttpClientRequest request) { + return request.getPeerPort(); } } diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientRequest.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientRequest.java new file mode 100644 index 000000000000..be616c847cc0 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientRequest.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import static java.util.logging.Level.FINE; + +import io.opentelemetry.context.Context; +import java.util.List; +import java.util.logging.Logger; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.net.URIAuthority; + +public class ApacheHttpClientRequest { + private static final Logger logger = Logger.getLogger(ApacheHttpClientRequest.class.getName()); + + private final Context parentContext; + private final HttpRequest httpRequest; + + public ApacheHttpClientRequest(Context parentContext, HttpRequest httpRequest) { + this.parentContext = parentContext; + this.httpRequest = httpRequest; + } + + public Context getParentContext() { + return parentContext; + } + + public String getPeerName() { + return httpRequest.getAuthority().getHostName(); + } + + public Integer getPeerPort() { + int port = httpRequest.getAuthority().getPort(); + if (port != -1) { + return port; + } + String scheme = httpRequest.getScheme(); + if (scheme == null) { + return 80; + } + switch (scheme) { + case "http": + return 80; + case "https": + return 443; + default: + logger.log(FINE, "no default port mapping for scheme: {0}", scheme); + return null; + } + } + + public String getMethod() { + return httpRequest.getMethod(); + } + + public String getUrl() { + // similar to org.apache.hc.core5.http.message.BasicHttpRequest.getUri() + // not calling getUri() to avoid unnecessary conversion + StringBuilder url = new StringBuilder(); + URIAuthority authority = httpRequest.getAuthority(); + if (authority != null) { + String scheme = httpRequest.getScheme(); + if (scheme != null) { + url.append(scheme); + url.append("://"); + } else { + url.append("http://"); + } + url.append(authority.getHostName()); + int port = authority.getPort(); + if (port >= 0) { + url.append(":"); + url.append(port); + } + } + String path = httpRequest.getPath(); + if (path != null) { + if (url.length() > 0 && !path.startsWith("/")) { + url.append("/"); + } + url.append(path); + } else { + url.append("/"); + } + return url.toString(); + } + + public String getFlavor() { + return ApacheHttpClientHelper.getFlavor(httpRequest.getVersion()); + } + + public List getHeader(String name) { + return ApacheHttpClientHelper.getHeader(httpRequest, name); + } + + public void setHeader(String key, String value) { + httpRequest.setHeader(key, value); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientSingletons.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientSingletons.java index ac2405b69754..bea7a6c6740f 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientSingletons.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientSingletons.java @@ -6,29 +6,32 @@ package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; 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.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; +import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; public final class ApacheHttpClientSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.apache-httpclient-5.0"; - private static final Instrumenter INSTRUMENTER; + private static final Instrumenter INSTRUMENTER; + private static final VirtualField metricsByContext; static { + metricsByContext = VirtualField.find(Context.class, BytesTransferMetrics.class); ApacheHttpClientHttpAttributesGetter httpAttributesGetter = new ApacheHttpClientHttpAttributesGetter(); ApacheHttpClientNetAttributesGetter netAttributesGetter = new ApacheHttpClientNetAttributesGetter(); INSTRUMENTER = - Instrumenter.builder( + Instrumenter.builder( GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, HttpSpanNameExtractor.create(httpAttributesGetter)) @@ -41,13 +44,27 @@ public final class ApacheHttpClientSingletons { .addAttributesExtractor( PeerServiceAttributesExtractor.create( netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) + .addAttributesExtractor(new ApacheHttpClientContentLengthAttributesGetter()) .addOperationMetrics(HttpClientMetrics.get()) .buildClientInstrumenter(HttpHeaderSetter.INSTANCE); } - public static Instrumenter instrumenter() { + public static Instrumenter instrumenter() { return INSTRUMENTER; } + public static BytesTransferMetrics createOrGetBytesTransferMetrics(Context parentContext) { + BytesTransferMetrics metrics = metricsByContext.get(parentContext); + if (metrics == null) { + metrics = new BytesTransferMetrics(); + metricsByContext.set(parentContext, metrics); + } + return metrics; + } + + public static BytesTransferMetrics getBytesTransferMetrics(Context parentContext) { + return metricsByContext.get(parentContext); + } + private ApacheHttpClientSingletons() {} } diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/BytesTransferMetrics.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/BytesTransferMetrics.java new file mode 100644 index 000000000000..f523add39d0b --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/BytesTransferMetrics.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import java.util.concurrent.atomic.AtomicLong; + +public class BytesTransferMetrics { + private final AtomicLong bytesOut = new AtomicLong(); + + private final AtomicLong bytesIn = new AtomicLong(); + + private long requestContentLength = -1; + + private long responseContentLength = -1; + + /** + * Set content-length of request sent by the client. In case of chunked request, the value + * returned by the client is -1. + * + * @param contentLength request content length or -ve value for chunked. + */ + public void setRequestContentLength(long contentLength) { + this.requestContentLength = contentLength; + } + + /** + * Set content-length of response received by the client. In case of chunked response, the value + * returned by the client is -1. + * + * @param contentLength response content length or -ve value for chunked. + */ + public void setResponseContentLength(long contentLength) { + this.responseContentLength = contentLength; + } + + /** + * Add request bytes produced. This may not always represent the request size, for example in case + * when connection is closed while writing request, this will repsent the bytes written till this + * point. + * + * @param byteLength bytes written from request. + */ + public void addRequestBytes(int byteLength) { + bytesOut.addAndGet(byteLength); + } + + /** + * Add response bytes consumed. This may not always represent the response size, for example in + * case when connection is closed while reading response, this will represent the bytes read till + * this point. + * + *

**Note** that this metric may only be applicable for async client, since it fetches response + * eagerly but, sync client fetches response lazily and hence the bytes may even be consumed after + * the end of span. + * + * @param byteLength bytes consumed from response. + */ + public void addResponseBytes(int byteLength) { + bytesIn.addAndGet(byteLength); + } + + /** + * Get request content length, priority is given if explicit content-length is present like in + * case when the request is not chunked, else value is computed using the bytes written. + * + * @return content-length of request, null if content-length is not applicable. + */ + public Long getRequestContentLength() { + if (requestContentLength >= 0) { + return requestContentLength; + } + long bytesWritten = bytesOut.get(); + if (bytesWritten > 0) { + return bytesWritten; + } + return null; + } + + /** + * Get response content length, priority is given if explicit content-length is present like in + * case when the response is not chunked, else value is computed using the bytes read. + * + * @return content-length of response, null if content-length is not applicable. + */ + public Long getResponseContentLength() { + if (responseContentLength >= 0) { + return responseContentLength; + } + long bytesRead = bytesIn.get(); + if (bytesRead > 0) { + return bytesRead; + } + return null; + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/CountingOutputStream.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/CountingOutputStream.java new file mode 100644 index 000000000000..a3ba3f00c1d8 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/CountingOutputStream.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0.ApacheHttpClientSingletons.createOrGetBytesTransferMetrics; + +import io.opentelemetry.context.Context; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.concurrent.atomic.AtomicBoolean; + +public class CountingOutputStream extends FilterOutputStream { + private final BytesTransferMetrics metrics; + private final AtomicBoolean closed; + + /** + * Wraps another output stream, counting the number of bytes written. + * + * @param out the output stream to be wrapped + */ + public CountingOutputStream(Context parentContext, OutputStream out) { + super(out); + this.metrics = createOrGetBytesTransferMetrics(parentContext); + this.closed = new AtomicBoolean(false); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + if (!closed.get()) { + metrics.addRequestBytes(len); + } + } + + @Override + public void write(int b) throws IOException { + out.write(b); + if (!closed.get()) { + metrics.addRequestBytes(1); + } + } + + // Overriding close() because FilterOutputStream's close() method pre-JDK8 has bad behavior: + // it silently ignores any exception thrown by flush(). Instead, just close the delegate stream. + // It should flush itself if necessary. + @Override + public void close() throws IOException { + out.close(); + closed.compareAndSet(false, true); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/HttpHeaderSetter.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/HttpHeaderSetter.java index 292d6642dc03..1e250de1e4c7 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/HttpHeaderSetter.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/HttpHeaderSetter.java @@ -7,13 +7,12 @@ import io.opentelemetry.context.propagation.TextMapSetter; import javax.annotation.Nullable; -import org.apache.hc.core5.http.HttpRequest; -enum HttpHeaderSetter implements TextMapSetter { +enum HttpHeaderSetter implements TextMapSetter { INSTANCE; @Override - public void set(@Nullable HttpRequest carrier, String key, String value) { + public void set(@Nullable ApacheHttpClientRequest carrier, String key, String value) { if (carrier == null) { return; } diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/RequestWithHost.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/RequestWithHost.java index 6fd5767e83a8..b4f48fbcd3b5 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/RequestWithHost.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/RequestWithHost.java @@ -5,7 +5,6 @@ package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; -import java.net.URI; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpHost; @@ -13,15 +12,16 @@ import org.apache.hc.core5.net.URIAuthority; public class RequestWithHost extends HttpRequestWrapper implements ClassicHttpRequest { - private final String scheme; private final URIAuthority authority; + private final ClassicHttpRequest httpRequest; public RequestWithHost(HttpHost httpHost, ClassicHttpRequest httpRequest) { super(httpRequest); this.scheme = httpHost.getSchemeName(); this.authority = new URIAuthority(httpHost.getHostName(), httpHost.getPort()); + this.httpRequest = httpRequest; } @Override @@ -34,20 +34,13 @@ public URIAuthority getAuthority() { return authority; } - @Override - public URI getUri() { - // overriding super because it's not correct (doesn't incorporate authority) - // and isn't needed anyways - throw new UnsupportedOperationException(); - } - @Override public HttpEntity getEntity() { - throw new UnsupportedOperationException(); + return httpRequest.getEntity(); } @Override - public void setEntity(HttpEntity entity) { - throw new UnsupportedOperationException(); + public void setEntity(HttpEntity httpEntity) { + httpRequest.setEntity(httpEntity); } } diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/WrappedHttpEntity.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/WrappedHttpEntity.java new file mode 100644 index 000000000000..698ba0d498f7 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/WrappedHttpEntity.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import io.opentelemetry.context.Context; +import java.io.IOException; +import java.io.OutputStream; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.entity.HttpEntityWrapper; + +public class WrappedHttpEntity extends HttpEntityWrapper { + private final Context parentContext; + + public WrappedHttpEntity(Context parentContext, HttpEntity wrappedEntity) { + super(wrappedEntity); + this.parentContext = parentContext; + } + + @Override + public void writeTo(OutputStream outStream) throws IOException { + super.writeTo(new CountingOutputStream(parentContext, outStream)); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/WrappingStatusSettingResponseHandler.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/WrappingStatusSettingResponseHandler.java index 4735955e5775..09d4a0395f79 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/WrappingStatusSettingResponseHandler.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/WrappingStatusSettingResponseHandler.java @@ -10,7 +10,6 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import java.io.IOException; -import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.io.HttpClientResponseHandler; @@ -18,23 +17,23 @@ public class WrappingStatusSettingResponseHandler implements HttpClientResponseHandler { final Context context; final Context parentContext; - final ClassicHttpRequest request; + final ApacheHttpClientRequest otelRequest; final HttpClientResponseHandler handler; public WrappingStatusSettingResponseHandler( Context context, Context parentContext, - ClassicHttpRequest request, + ApacheHttpClientRequest otelRequest, HttpClientResponseHandler handler) { this.context = context; this.parentContext = parentContext; - this.request = request; + this.otelRequest = otelRequest; this.handler = handler; } @Override public T handleResponse(ClassicHttpResponse response) throws IOException, HttpException { - instrumenter().end(context, request, response, null); + instrumenter().end(context, otelRequest, response, null); // ending the span before executing the callback handler (and scoping the callback handler to // the parent context), even though we are inside of a synchronous http client callback // underneath HttpClient.execute(..), in order to not attribute other CLIENT span timings that diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/AbstractApacheHttpClientTest.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/AbstractApacheHttpClientTest.java index b3d9d0f2e94a..8a345539859b 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/AbstractApacheHttpClientTest.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/AbstractApacheHttpClientTest.java @@ -44,6 +44,8 @@ protected Set> getHttpAttributes(URI endpoint) { attributes.add(SemanticAttributes.NET_PEER_PORT); attributes.add(SemanticAttributes.HTTP_URL); attributes.add(SemanticAttributes.HTTP_METHOD); + attributes.add(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH); + attributes.add(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH); if (endpoint.toString().contains("/success")) { attributes.add(SemanticAttributes.HTTP_FLAVOR); }