Skip to content

Commit

Permalink
Add async apache HTTP client body and headers capture (#190)
Browse files Browse the repository at this point in the history
* Add body capture for async Apache client

Signed-off-by: Pavol Loffay <p.loffay@gmail.com>

* Some progress but failing

Signed-off-by: Pavol Loffay <p.loffay@gmail.com>

* Some fixes

Signed-off-by: Pavol Loffay <p.loffay@gmail.com>

* Working request body

Signed-off-by: Pavol Loffay <p.loffay@gmail.com>

* working

Signed-off-by: Pavol Loffay <p.loffay@gmail.com>

* Add readme

Signed-off-by: Pavol Loffay <p.loffay@gmail.com>

* check content types

Signed-off-by: Pavol Loffay <p.loffay@gmail.com>

* Fix

Signed-off-by: Pavol Loffay <p.loffay@gmail.com>
  • Loading branch information
pavolloffay authored Dec 21, 2020
1 parent b54e8b3 commit c320487
Show file tree
Hide file tree
Showing 16 changed files with 746 additions and 189 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and adds following capabilities:
List of supported frameworks with additional capabilities:
| Library/Framework | Versions |
|--------------------------------------------------------------------------------------------------------|-----------------|
| [Apache HttpAsyncClient](https://hc.apache.org/index.html) | 4.1+ |
| [Apache HttpClient](https://hc.apache.org/index.html) | 4.0+ |
| [gRPC](https://github.com/grpc/grpc-java) | 1.5+ |
| [JAX-RS Client](https://javaee.github.io/javaee-spec/javadocs/javax/ws/rs/client/package-summary.html) | 2.0+ |
Expand Down
39 changes: 39 additions & 0 deletions instrumentation/apache-httpasyncclient-4.1/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
plugins {
`java-library`
id("net.bytebuddy.byte-buddy")
id("io.opentelemetry.instrumentation.auto-instrumentation")
muzzle
}

muzzle {
pass {
group = "org.apache.httpcomponents"
module = "httpasyncclient"
// 4.0 and 4.0.1 don't copy over the traceparent (etc) http headers on redirect
versions = "[4.1,)"
// TODO implement a muzzle check so that 4.0.x (at least 4.0 and 4.0.1) do not get applied
// and then bring back assertInverse
}
}

afterEvaluate{
io.opentelemetry.instrumentation.gradle.bytebuddy.ByteBuddyPluginConfigurator(project,
sourceSets.main.get(),
"io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin",
project(":javaagent-tooling").configurations["instrumentationMuzzle"] + configurations.runtimeClasspath
).configure()
}

val versions: Map<String, String> by extra

dependencies {
api(project(":instrumentation:java-streams"))
api(project(":instrumentation:apache-httpclient-4.0"))

api("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-apache-httpasyncclient-4.1:${versions["opentelemetry_java_agent"]}")

implementation("org.apache.httpcomponents:httpasyncclient:4.1")

testImplementation(project(":testing-common"))
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright The Hypertrace Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient;

import io.opentelemetry.context.Context;
import io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient.ApacheHttpAsyncClientInstrumentation.DelegatingRequestProducer;

/**
* TODO remove once https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/1951
* is merged
*/
public class DelegatingRequestAccessor {

public static Context get(DelegatingRequestProducer delegatingRequestProducer) {
return delegatingRequestProducer.context;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* Copyright The Hypertrace Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.opentelemetry.javaagent.instrumentation.hypertrace.apachehttpasyncclient;

import static io.opentelemetry.javaagent.tooling.ClassLoaderMatcher.hasClassesNamed;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import com.google.auto.service.AutoService;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient.ApacheHttpAsyncClientInstrumentation.DelegatingRequestProducer;
import io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient.DelegatingRequestAccessor;
import io.opentelemetry.javaagent.instrumentation.hypertrace.apachehttpclient.v4_0.ApacheHttpClientUtils;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.nio.protocol.HttpAsyncRequestProducer;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpCoreContext;

@AutoService(InstrumentationModule.class)
public class ApacheAsyncClientInstrumentationModule extends InstrumentationModule {

public ApacheAsyncClientInstrumentationModule() {
super(
ApacheAsyncHttpClientInstrumentationName.PRIMARY,
ApacheAsyncHttpClientInstrumentationName.OTHER);
}

@Override
public int getOrder() {
return 1;
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return Collections.singletonList(new HttpAsyncClientInstrumentation());
}

class HttpAsyncClientInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("org.apache.http.nio.client.HttpAsyncClient");
}

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return implementsInterface(named("org.apache.http.nio.client.HttpAsyncClient"));
}

@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
isMethod()
.and(named("execute"))
.and(takesArguments(4))
.and(takesArgument(0, named("org.apache.http.nio.protocol.HttpAsyncRequestProducer")))
.and(
takesArgument(1, named("org.apache.http.nio.protocol.HttpAsyncResponseConsumer")))
.and(takesArgument(2, named("org.apache.http.protocol.HttpContext")))
.and(takesArgument(3, named("org.apache.http.concurrent.FutureCallback"))),
ApacheAsyncClientInstrumentationModule.class.getName()
+ "$HttpAsyncClient_execute_Advice");
}
}

public static class HttpAsyncClient_execute_Advice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void enter(
@Advice.Argument(value = 0, readOnly = false) HttpAsyncRequestProducer requestProducer,
@Advice.Argument(value = 2) HttpContext httpContext,
@Advice.Argument(value = 3, readOnly = false) FutureCallback futureCallback) {
if (requestProducer instanceof DelegatingRequestProducer) {
DelegatingRequestProducer delegatingRequestProducer =
(DelegatingRequestProducer) requestProducer;
Context context = DelegatingRequestAccessor.get(delegatingRequestProducer);
requestProducer = new DelegatingCaptureBodyRequestProducer(context, requestProducer);
futureCallback = new BodyCaptureDelegatingCallback(context, httpContext, futureCallback);
}
}
}

public static class DelegatingCaptureBodyRequestProducer extends DelegatingRequestProducer {

final Context context;

public DelegatingCaptureBodyRequestProducer(
Context context, HttpAsyncRequestProducer delegate) {
super(context, delegate);
this.context = context;
}

@Override
public HttpRequest generateRequest() throws IOException, HttpException {
HttpRequest request = super.generateRequest();
ApacheHttpClientUtils.traceRequest(Span.fromContext(context), request);
return request;
}
}

public static class BodyCaptureDelegatingCallback<T> implements FutureCallback<T> {

final Context context;
final FutureCallback<T> delegate;
final HttpContext httpContext;

public BodyCaptureDelegatingCallback(
Context context, HttpContext httpContext, FutureCallback<T> delegate) {
this.delegate = delegate;
this.context = context;
this.httpContext = httpContext;
}

@Override
public void completed(T result) {
HttpResponse httpResponse = getResponse(httpContext);
ApacheHttpClientUtils.traceResponse(Span.fromContext(context), httpResponse);
delegate.completed(result);
}

@Override
public void failed(Exception ex) {
HttpResponse httpResponse = getResponse(httpContext);
ApacheHttpClientUtils.traceResponse(Span.fromContext(context), httpResponse);
delegate.failed(ex);
}

@Override
public void cancelled() {
HttpResponse httpResponse = getResponse(httpContext);
ApacheHttpClientUtils.traceResponse(Span.fromContext(context), httpResponse);
delegate.cancelled();
}

private static HttpResponse getResponse(HttpContext context) {
return (HttpResponse) context.getAttribute(HttpCoreContext.HTTP_RESPONSE);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright The Hypertrace Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.opentelemetry.javaagent.instrumentation.hypertrace.apachehttpasyncclient;

public class ApacheAsyncHttpClientInstrumentationName {

public static final String PRIMARY = "apache-httpasyncclient";
public static final String[] OTHER = {
"apache-httpasyncclient-4.1",
"ht",
"apache-httpasyncclient-ht",
"apache-httpasyncclient-4.1-ht",
};
}
Loading

0 comments on commit c320487

Please sign in to comment.