Add akka-http-client instrumentation
This is a very-very first pass: instrument single request
This commit is contained in:
parent
6932d581ed
commit
2b25de966a
|
@ -41,3 +41,6 @@ dependencies {
|
||||||
|
|
||||||
test.dependsOn lagomTest
|
test.dependsOn lagomTest
|
||||||
testJava8Minimum += '*Test*.class'
|
testJava8Minimum += '*Test*.class'
|
||||||
|
|
||||||
|
// These classes use Ratpack which requires Java 8. (Currently also incompatible with Java 9.)
|
||||||
|
testJava8Only += '**/AkkaHttpClientInstrumentationTest.class'
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
package datadog.trace.instrumentation.akkahttp;
|
||||||
|
|
||||||
|
import static io.opentracing.log.Fields.ERROR_OBJECT;
|
||||||
|
import static net.bytebuddy.matcher.ElementMatchers.*;
|
||||||
|
|
||||||
|
import akka.http.javadsl.model.headers.RawHeader;
|
||||||
|
import akka.http.scaladsl.HttpExt;
|
||||||
|
import akka.http.scaladsl.model.HttpRequest;
|
||||||
|
import akka.http.scaladsl.model.HttpResponse;
|
||||||
|
import akka.stream.*;
|
||||||
|
import com.google.auto.service.AutoService;
|
||||||
|
import datadog.trace.agent.tooling.*;
|
||||||
|
import datadog.trace.api.DDSpanTypes;
|
||||||
|
import datadog.trace.api.DDTags;
|
||||||
|
import io.opentracing.Scope;
|
||||||
|
import io.opentracing.Span;
|
||||||
|
import io.opentracing.propagation.Format;
|
||||||
|
import io.opentracing.propagation.TextMap;
|
||||||
|
import io.opentracing.tag.Tags;
|
||||||
|
import io.opentracing.util.GlobalTracer;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.bytebuddy.agent.builder.AgentBuilder;
|
||||||
|
import net.bytebuddy.asm.Advice;
|
||||||
|
import scala.concurrent.Future;
|
||||||
|
import scala.runtime.AbstractFunction1;
|
||||||
|
import scala.util.Try;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@AutoService(Instrumenter.class)
|
||||||
|
public final class AkkaHttpClientInstrumentation extends Instrumenter.Configurable {
|
||||||
|
public AkkaHttpClientInstrumentation() {
|
||||||
|
super("akka-http", "akka-http-client");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean defaultEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final HelperInjector HELPER_INJECTOR =
|
||||||
|
new HelperInjector(
|
||||||
|
AkkaHttpClientInstrumentation.class.getName() + "$OnCompleteHandler",
|
||||||
|
AkkaHttpClientInstrumentation.class.getName() + "$AkkaHttpHeaders");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AgentBuilder apply(final AgentBuilder agentBuilder) {
|
||||||
|
return agentBuilder
|
||||||
|
.type(named("akka.http.scaladsl.HttpExt"))
|
||||||
|
.transform(DDTransformers.defaultTransformers())
|
||||||
|
.transform(HELPER_INJECTOR)
|
||||||
|
.transform(
|
||||||
|
DDAdvice.create()
|
||||||
|
.advice(
|
||||||
|
named("singleRequest")
|
||||||
|
.and(takesArgument(0, named("akka.http.scaladsl.model.HttpRequest"))),
|
||||||
|
AkkaHttpClientAdvice.class.getName()))
|
||||||
|
.asDecorator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AkkaHttpClientAdvice {
|
||||||
|
@Advice.OnMethodEnter(suppress = Throwable.class)
|
||||||
|
public static Scope methodEnter(
|
||||||
|
@Advice.Argument(value = 0, readOnly = false) HttpRequest request) {
|
||||||
|
Scope scope =
|
||||||
|
GlobalTracer.get()
|
||||||
|
.buildSpan("akka-http.request")
|
||||||
|
.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
|
||||||
|
.withTag(Tags.HTTP_METHOD.getKey(), request.method().value())
|
||||||
|
.withTag(DDTags.SPAN_TYPE, DDSpanTypes.HTTP_CLIENT)
|
||||||
|
.withTag(Tags.COMPONENT.getKey(), "akka-http-client")
|
||||||
|
.withTag(Tags.HTTP_URL.getKey(), request.getUri().toString())
|
||||||
|
.startActive(false);
|
||||||
|
|
||||||
|
AkkaHttpHeaders headers = new AkkaHttpHeaders(request);
|
||||||
|
GlobalTracer.get().inject(scope.span().context(), Format.Builtin.HTTP_HEADERS, headers);
|
||||||
|
// Request is immutable, so we have to assign new value once we update headers
|
||||||
|
request = headers.getRequest();
|
||||||
|
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Advice.OnMethodExit(suppress = Throwable.class)
|
||||||
|
public static void methodExit(
|
||||||
|
@Advice.Argument(value = 0) final HttpRequest request,
|
||||||
|
@Advice.This final HttpExt thiz,
|
||||||
|
@Advice.Return final Future<HttpResponse> responseFuture,
|
||||||
|
@Advice.Enter final Scope scope) {
|
||||||
|
responseFuture.onComplete(new OnCompleteHandler(scope), thiz.system().dispatcher());
|
||||||
|
scope.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class OnCompleteHandler extends AbstractFunction1<Try<HttpResponse>, Void> {
|
||||||
|
private final Scope scope;
|
||||||
|
|
||||||
|
public OnCompleteHandler(Scope scope) {
|
||||||
|
this.scope = scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Void apply(Try<HttpResponse> result) {
|
||||||
|
Span span = scope.span();
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
Tags.HTTP_STATUS.set(span, result.get().status().intValue());
|
||||||
|
} else {
|
||||||
|
Tags.ERROR.set(span, true);
|
||||||
|
span.log(Collections.singletonMap(ERROR_OBJECT, result.failed().get()));
|
||||||
|
}
|
||||||
|
span.finish();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AkkaHttpHeaders implements TextMap {
|
||||||
|
private HttpRequest request;
|
||||||
|
|
||||||
|
public AkkaHttpHeaders(HttpRequest request) {
|
||||||
|
this.request = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<Map.Entry<String, String>> iterator() {
|
||||||
|
throw new UnsupportedOperationException(
|
||||||
|
"This class should be used only with Tracer.inject()!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void put(final String name, final String value) {
|
||||||
|
// It looks like this cast is only needed in Java, Scala would have figured it out
|
||||||
|
request = (HttpRequest) request.addHeader(RawHeader.create(name, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpRequest getRequest() {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
import akka.actor.ActorSystem
|
||||||
|
import akka.http.javadsl.Http
|
||||||
|
import akka.http.javadsl.model.HttpRequest
|
||||||
|
import akka.http.javadsl.model.HttpResponse
|
||||||
|
import akka.stream.ActorMaterializer
|
||||||
|
import akka.stream.StreamTcpException
|
||||||
|
import datadog.trace.agent.test.AgentTestRunner
|
||||||
|
import datadog.trace.agent.test.RatpackUtils
|
||||||
|
import datadog.trace.api.DDSpanTypes
|
||||||
|
import datadog.trace.api.DDTags
|
||||||
|
import io.opentracing.tag.Tags
|
||||||
|
import spock.lang.Shared
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletionStage
|
||||||
|
import java.util.concurrent.ExecutionException
|
||||||
|
|
||||||
|
import static datadog.trace.agent.test.ListWriterAssert.assertTraces
|
||||||
|
import static ratpack.groovy.test.embed.GroovyEmbeddedApp.ratpack
|
||||||
|
|
||||||
|
class AkkaHttpClientInstrumentationTest extends AgentTestRunner {
|
||||||
|
static {
|
||||||
|
System.setProperty("dd.integration.akka-http-client.enabled", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String MESSAGE = "an\nmultiline\nhttp\nresponse"
|
||||||
|
private static final long TIMEOUT = 10000L
|
||||||
|
|
||||||
|
@Shared
|
||||||
|
def server = ratpack {
|
||||||
|
handlers {
|
||||||
|
prefix("success") {
|
||||||
|
all {
|
||||||
|
RatpackUtils.handleDistributedRequest(context)
|
||||||
|
|
||||||
|
response.status(200).send(MESSAGE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix("error") {
|
||||||
|
all {
|
||||||
|
RatpackUtils.handleDistributedRequest(context)
|
||||||
|
|
||||||
|
throw new RuntimeException("error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Shared
|
||||||
|
ActorSystem system = ActorSystem.create()
|
||||||
|
@Shared
|
||||||
|
ActorMaterializer materializer = ActorMaterializer.create(system)
|
||||||
|
|
||||||
|
def "#route request trace" () {
|
||||||
|
setup:
|
||||||
|
def url = server.address.resolve("/" + route).toURL()
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.create(url.toString())
|
||||||
|
CompletionStage<HttpResponse> responseFuture =
|
||||||
|
Http.get(system)
|
||||||
|
.singleRequest(request, materializer)
|
||||||
|
HttpResponse response = responseFuture.toCompletableFuture().get()
|
||||||
|
String message = readMessage(response)
|
||||||
|
|
||||||
|
expect:
|
||||||
|
response.status().intValue() == expectedStatus
|
||||||
|
if (expectedMessage != null) {
|
||||||
|
message == expectedMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTraces(TEST_WRITER, 2) {
|
||||||
|
trace(0, 1) {
|
||||||
|
span(0) {
|
||||||
|
operationName "test-http-server"
|
||||||
|
childOf(TEST_WRITER[1][0])
|
||||||
|
errored false
|
||||||
|
tags {
|
||||||
|
defaultTags()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trace(1, 1) {
|
||||||
|
span(0) {
|
||||||
|
parent()
|
||||||
|
serviceName "unnamed-java-app"
|
||||||
|
operationName "akka-http.request"
|
||||||
|
resourceName "GET /$route"
|
||||||
|
errored expectedError
|
||||||
|
tags {
|
||||||
|
defaultTags()
|
||||||
|
"$Tags.HTTP_STATUS.key" expectedStatus
|
||||||
|
"$Tags.HTTP_URL.key" "${server.address}$route"
|
||||||
|
"$Tags.HTTP_METHOD.key" "GET"
|
||||||
|
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
|
||||||
|
"$DDTags.SPAN_TYPE" DDSpanTypes.HTTP_CLIENT
|
||||||
|
"$Tags.COMPONENT.key" "akka-http-client"
|
||||||
|
if (expectedError) {
|
||||||
|
"$Tags.ERROR.key" true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
where:
|
||||||
|
route | expectedStatus | expectedError | expectedMessage
|
||||||
|
"success" | 200 | false | MESSAGE
|
||||||
|
"error" | 500 | true | null
|
||||||
|
}
|
||||||
|
|
||||||
|
def "error request trace" () {
|
||||||
|
setup:
|
||||||
|
def url = new URL("http://localhost:${server.address.port + 1}/test")
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.create(url.toString())
|
||||||
|
CompletionStage<HttpResponse> responseFuture =
|
||||||
|
Http.get(system)
|
||||||
|
.singleRequest(request, materializer)
|
||||||
|
try {
|
||||||
|
responseFuture.toCompletableFuture().get()
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
// This is expected to fail
|
||||||
|
}
|
||||||
|
|
||||||
|
expect:
|
||||||
|
assertTraces(TEST_WRITER, 1) {
|
||||||
|
trace(0, 1) {
|
||||||
|
span(0) {
|
||||||
|
parent()
|
||||||
|
serviceName "unnamed-java-app"
|
||||||
|
operationName "akka-http.request"
|
||||||
|
resourceName "GET /test"
|
||||||
|
errored true
|
||||||
|
tags {
|
||||||
|
defaultTags()
|
||||||
|
"$Tags.HTTP_URL.key" url.toString()
|
||||||
|
"$Tags.HTTP_METHOD.key" "GET"
|
||||||
|
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
|
||||||
|
"$DDTags.SPAN_TYPE" DDSpanTypes.HTTP_CLIENT
|
||||||
|
"$Tags.COMPONENT.key" "akka-http-client"
|
||||||
|
"$Tags.ERROR.key" true
|
||||||
|
errorTags(StreamTcpException, { it.contains("Tcp command") })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String readMessage(HttpResponse response) {
|
||||||
|
response.entity().toStrict(TIMEOUT, materializer).toCompletableFuture().get().getData().utf8String()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue