Skip to content

Commit

Permalink
add: instrumentation for apache http client 4.x (#1330)
Browse files Browse the repository at this point in the history
@PrajeenRG LGTM! Sorry about the delay getting back to you there. Merging 🚀
  • Loading branch information
PrajeenRG committed Mar 12, 2024
1 parent 355e749 commit 7b55e3f
Show file tree
Hide file tree
Showing 15 changed files with 1,050 additions and 3 deletions.
28 changes: 25 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ val instrumentationProjects = Seq[ProjectReference](
`kamon-aws-sdk`,
`kamon-alpakka-kafka`,
`kamon-http4s-1_0`,
`kamon-http4s-0_23`
`kamon-http4s-0_23`,
`kamon-apache-httpclient`
)

lazy val instrumentation = (project in file("instrumentation"))
Expand Down Expand Up @@ -820,6 +821,25 @@ lazy val `kamon-http4s-0_23` = (project in file("instrumentation/kamon-http4s-0.
`kamon-testkit` % Test
)

lazy val `kamon-apache-httpclient` = (project in file("instrumentation/kamon-apache-httpclient"))
.disablePlugins(AssemblyPlugin)
.enablePlugins(JavaAgent)
.settings(instrumentationSettings)
.settings(
libraryDependencies ++= Seq(
kanelaAgent % "provided",
"org.apache.httpcomponents" % "httpclient" % "4.0" % "provided",
slf4jApi % "provided",

scalatest % "test",
logbackClassic % "test",
"org.mock-server" % "mockserver-client-java" % "5.13.2" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.41.0" % "test",
"com.dimafeng" %% "testcontainers-scala-mockserver" % "0.41.0" % "test"
)
).dependsOn(`kamon-core`, `kamon-executors`, `kamon-testkit` % "test")


/**
* Reporters
*/
Expand Down Expand Up @@ -1087,7 +1107,8 @@ lazy val `kamon-bundle-dependencies-all` = (project in file("bundle/kamon-bundle
`kamon-okhttp`,
`kamon-caffeine`,
`kamon-lagom`,
`kamon-aws-sdk`
`kamon-aws-sdk`,
`kamon-apache-httpclient`
)

/**
Expand Down Expand Up @@ -1151,7 +1172,8 @@ lazy val `kamon-bundle-dependencies-3` = (project in file("bundle/kamon-bundle-d
`kamon-zio-2`,
`kamon-pekko`,
`kamon-pekko-http`,
`kamon-pekko-grpc`
`kamon-pekko-grpc`,
`kamon-apache-httpclient`
)

lazy val `kamon-bundle` = (project in file("bundle/kamon-bundle"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package kamon.instrumentation.apache.httpclient;

import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;

import kamon.instrumentation.http.HttpClientInstrumentation.RequestHandler;
import kamon.instrumentation.http.HttpMessage.RequestBuilder;
import kamon.trace.Span;
import kamon.Kamon;
import kamon.context.Context;
import kamon.context.Storage.Scope;
import kamon.instrumentation.context.HasContext;

import kanela.agent.libs.net.bytebuddy.asm.Advice;

public class RequestAdvisor {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(@Advice.Argument(0) HttpHost host,
@Advice.Argument(value = 1, readOnly = false) HttpRequest request,
@Advice.Local("handler") RequestHandler<HttpRequest> handler,
@Advice.Local("scope") Scope scope) {
if (((HasContext) request).context().nonEmpty()) {
// Request has been instrumented already
return;
}
final Context parentContext = Kamon.currentContext();
final RequestBuilder<HttpRequest> builder = ApacheHttpClientHelper.toRequestBuilder(host, request);
handler = ApacheHttpClientInstrumentation.httpClientInstrumentation().createHandler(builder, parentContext);
final Context ctx = parentContext.withEntry(Span.Key(), handler.span());
scope = Kamon.storeContext(ctx);
request = handler.request();
((HasContext) request).setContext(ctx);
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(@Advice.Return HttpResponse response,
@Advice.Thrown Throwable t,
@Advice.Local("handler") RequestHandler<HttpRequest> handler,
@Advice.Local("scope") Scope scope) {
if (scope == null) {
return;
}
ApacheHttpClientInstrumentation.processResponse(handler, response, t);
scope.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package kamon.instrumentation.apache.httpclient;

import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.client.ResponseHandler;

import kamon.Kamon;
import kamon.context.Context;
import kamon.context.Storage.Scope;
import kamon.instrumentation.context.HasContext;
import kamon.instrumentation.http.HttpClientInstrumentation.RequestHandler;
import kamon.instrumentation.http.HttpMessage.RequestBuilder;
import kamon.trace.Span;
import kanela.agent.libs.net.bytebuddy.asm.Advice;

public class RequestWithHandlerAdvisor {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(@Advice.Argument(0) HttpHost host,
@Advice.Argument(value = 1, readOnly = false) HttpRequest request,
@Advice.Argument(value = 2, readOnly = false) ResponseHandler<?> resHandler,
@Advice.Local("handler") RequestHandler<HttpRequest> reqHandler,
@Advice.Local("scope") Scope scope) {
if (((HasContext) request).context().nonEmpty()) {
// Request has been instrumented already
return;
}
final Context parentContext = Kamon.currentContext();
final RequestBuilder<HttpRequest> builder = ApacheHttpClientHelper.toRequestBuilder(host, request);
reqHandler = ApacheHttpClientInstrumentation.httpClientInstrumentation().createHandler(builder, parentContext);
resHandler = new ResponseHandlerProxy<>(reqHandler, resHandler, parentContext);
final Context ctx = parentContext.withEntry(Span.Key(), reqHandler.span());
scope = Kamon.storeContext(ctx);
request = reqHandler.request();
((HasContext) request).setContext(ctx);
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(@Advice.Local("scope") Scope scope) {
if (scope != null) {
scope.close();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package kamon.instrumentation.apache.httpclient;

import java.io.IOException;

import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;

import kamon.Kamon;
import kamon.context.Context;
import kamon.context.Storage.Scope;
import kamon.instrumentation.http.HttpClientInstrumentation.RequestHandler;

public class ResponseHandlerProxy<T, U> implements ResponseHandler<T> {

private final ResponseHandler<T> delegate;
private final RequestHandler<U> handler;
private Context parentContext;

public ResponseHandlerProxy(RequestHandler<U> handler, ResponseHandler<T> delegate, Context parentContext) {
this.handler = handler;
this.delegate = delegate;
this.parentContext = parentContext;
}

@Override
public T handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
ApacheHttpClientInstrumentation.processResponse(handler, response, null);
// run original handler in parent context to avoid nesting of spans
try (Scope ignored = Kamon.storeContext(parentContext)) {
return delegate.handleResponse(response);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package kamon.instrumentation.apache.httpclient;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpUriRequest;

import kamon.Kamon;
import kamon.context.Context;
import kamon.context.Storage.Scope;
import kamon.instrumentation.context.HasContext;
import kamon.instrumentation.http.HttpClientInstrumentation.RequestHandler;
import kamon.instrumentation.http.HttpMessage.RequestBuilder;
import kamon.trace.Span;
import kanela.agent.libs.net.bytebuddy.asm.Advice;

public class UriRequestAdvisor {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(@Advice.Argument(value = 0, readOnly = false) HttpUriRequest request,
@Advice.Local("handler") RequestHandler<HttpUriRequest> handler,
@Advice.Local("scope") Scope scope) {
if (((HasContext) request).context().nonEmpty()) {
// Request has been instrumented already
return;
}
final Context parentContext = Kamon.currentContext();
final RequestBuilder<HttpUriRequest> builder = ApacheHttpClientHelper.toRequestBuilder(request);
handler = ApacheHttpClientInstrumentation.httpClientInstrumentation().createHandler(builder, parentContext);
final Context ctx = parentContext.withEntry(Span.Key(), handler.span());
scope = Kamon.storeContext(ctx);
request = handler.request();
((HasContext) request).setContext(ctx);
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(@Advice.Return HttpResponse response,
@Advice.Thrown Throwable t,
@Advice.Local("handler") RequestHandler<HttpUriRequest> handler,
@Advice.Local("scope") Scope scope) {
if (scope == null) {
return;
}
ApacheHttpClientInstrumentation.processResponse(handler, response, t);
scope.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package kamon.instrumentation.apache.httpclient;

import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpUriRequest;

import kamon.Kamon;
import kamon.context.Context;
import kamon.context.Storage.Scope;
import kamon.instrumentation.context.HasContext;
import kamon.instrumentation.http.HttpClientInstrumentation.RequestHandler;
import kamon.instrumentation.http.HttpMessage.RequestBuilder;
import kamon.trace.Span;
import kanela.agent.libs.net.bytebuddy.asm.Advice;

public class UriRequestWithHandlerAdvisor {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.Argument(value = 0, readOnly = false) HttpUriRequest request,
@Advice.Argument(value = 1, readOnly = false) ResponseHandler<?> resHandler,
@Advice.Local("handler") RequestHandler<HttpUriRequest> reqHandler,
@Advice.Local("scope") Scope scope) {
if (((HasContext) request).context().nonEmpty()) {
// Request has been instrumented already
return;
}
final Context parentContext = Kamon.currentContext();
final RequestBuilder<HttpUriRequest> builder = ApacheHttpClientHelper.toRequestBuilder(request);
reqHandler = ApacheHttpClientInstrumentation.httpClientInstrumentation().createHandler(builder, parentContext);
resHandler = new ResponseHandlerProxy<>(reqHandler, resHandler, parentContext);
final Context ctx = parentContext.withEntry(Span.Key(), reqHandler.span());
scope = Kamon.storeContext(ctx);
request = reqHandler.request();
((HasContext) request).setContext(ctx);
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(@Advice.Local("scope") Scope scope) {
if (scope != null) {
scope.close();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# ================================================== #
# kamon Apache HttpClient 2.0 client reference configuration #
# ================================================== #

# Settings to control the HTTP Client instrumentation
#
# IMPORTANT: The entire configuration of the HTTP Client Instrumentation is based on the constructs provided by the
# Kamon Instrumentation Common library which will always fallback to the settings found under the
# "kamon.instrumentation.http-client.default" path. The default settings have been included here to make them easy to
# find and understand in the context of this project and commented out so that any changes to the default settings
# will actually have effect.
#
kamon.instrumentation.apache.httpclient {

#
# Configuration for HTTP context propagation.
#
propagation {

# Enables or disables HTTP context propagation on this HTTP client instrumentation. Please note that if
# propagation is disabled then some distributed tracing features will not be work as expected (e.g. Spans can
# be created and reported but will not be linked across boundaries nor take trace identifiers from tags).
#enabled = yes

# HTTP propagation channel to b used by this instrumentation. Take a look at the kamon.propagation.http.default
# configuration for more details on how to configure the detault HTTP context propagation.
#channel = "default"
}

tracing {

# Enables HTTP request tracing. When enabled the instrumentation will create Spans for outgoing requests
# and finish them when the response is received from the server.
#enabled = yes

# Enables collection of span metrics using the `span.processing-time` metric.
#span-metrics = on

# Select which tags should be included as span and span metric tags. The possible options are:
# - span: the tag is added as a Span tag (i.e. using span.tag(...))
# - metric: the tag is added a a Span metric tag (i.e. using span.tagMetric(...))
# - off: the tag is not used.
#
tags {

# Use the http.url tag.
#url = span

# Use the http.method tag.
#method = metric

# Use the http.status_code tag.
#status-code = metric

# Copy tags from the context into the Spans with the specified purpouse. For example, to copy a customer_type
# tag from the context into the HTTP Client Span created by the instrumentation, the following configuration
# should be added:
#
# from-context {
# customer_type = span
# }
#
from-context {

}
}

operations {

# The default operation name to be used when creating Spans to handle the HTTP client requests. The HTTP
# Client instrumentation will always try to use the HTTP Operation Name Generator configured below to get
# a name, but if it fails to generate it then this name will be used.
#default = "http.client.request"

# FQCN for a HttpOperationNameGenerator implementation, or ony of the following shorthand forms:
# - hostname: Uses the request Host as the operation name.
# - method: Uses the request HTTP method as the operation name.
#
#name-generator = "method"
}
}

}

kanela {
modules {
apache-httpclient {
name = "Apache Http Client"
description = "Provides tracing of client calls made with the official Apache HttpClient library."
instrumentations = [
"kamon.instrumentation.apache.httpclient.ApacheHttpClientInstrumentation"
]

within = [
"org.apache.http..*"
]
}
}
}
Loading

0 comments on commit 7b55e3f

Please sign in to comment.