Create javaagent dirs for all instrumentations (#1668)

* Create javaagent dirs for all instrumentation

* Add note about kotlin coroutine library instrumentation

* Feedback
This commit is contained in:
Trask Stalnaker 2020-11-28 21:04:16 -08:00 committed by GitHub
parent f2bb2f3e30
commit 5f263644da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
892 changed files with 63421 additions and 21 deletions

View File

@ -0,0 +1,18 @@
apply from: "$rootDir/gradle/instrumentation.gradle"
apply from: "$rootDir/gradle/test-with-scala.gradle"
muzzle {
pass {
group = 'com.typesafe.akka'
module = 'akka-actor_2.11'
versions = "[2.5.0,)"
}
}
dependencies {
implementation project(':instrumentation:executors:javaagent')
compileOnly group: 'com.typesafe.akka', name: 'akka-actor_2.11', version: '2.5.0'
testImplementation deps.scala
testImplementation group: 'com.typesafe.akka', name: 'akka-actor_2.11', version: '2.5.0'
}

View File

@ -0,0 +1,44 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.akkaactor;
import static java.util.Arrays.asList;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.instrumentation.api.concurrent.State;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
@AutoService(InstrumentationModule.class)
public class AkkaActorInstrumentationModule extends InstrumentationModule {
public AkkaActorInstrumentationModule() {
super("akka-actor", "akka-actor-2.5");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return asList(new AkkaForkJoinPoolInstrumentation(), new AkkaForkJoinTaskInstrumentation());
}
@Override
public Map<String, String> contextStore() {
Map<String, String> map = new HashMap<>();
map.put(Runnable.class.getName(), State.class.getName());
map.put(Callable.class.getName(), State.class.getName());
map.put(AkkaForkJoinTaskInstrumentation.TASK_CLASS_NAME, State.class.getName());
return Collections.unmodifiableMap(map);
}
@Override
protected boolean defaultEnabled() {
return false;
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.akkaactor;
import static net.bytebuddy.matcher.ElementMatchers.nameMatches;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import akka.dispatch.forkjoin.ForkJoinTask;
import io.opentelemetry.javaagent.instrumentation.api.ContextStore;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
import io.opentelemetry.javaagent.instrumentation.api.concurrent.ExecutorInstrumentationUtils;
import io.opentelemetry.javaagent.instrumentation.api.concurrent.State;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.HashMap;
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;
public class AkkaForkJoinPoolInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
// This might need to be an extendsClass matcher...
return named("akka.dispatch.forkjoin.ForkJoinPool");
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
Map<ElementMatcher<? super MethodDescription>, String> transformers = new HashMap<>();
transformers.put(
named("execute")
.and(takesArgument(0, named(AkkaForkJoinTaskInstrumentation.TASK_CLASS_NAME))),
AkkaForkJoinPoolInstrumentation.class.getName() + "$SetAkkaForkJoinStateAdvice");
transformers.put(
named("submit")
.and(takesArgument(0, named(AkkaForkJoinTaskInstrumentation.TASK_CLASS_NAME))),
AkkaForkJoinPoolInstrumentation.class.getName() + "$SetAkkaForkJoinStateAdvice");
transformers.put(
nameMatches("invoke")
.and(takesArgument(0, named(AkkaForkJoinTaskInstrumentation.TASK_CLASS_NAME))),
AkkaForkJoinPoolInstrumentation.class.getName() + "$SetAkkaForkJoinStateAdvice");
return transformers;
}
public static class SetAkkaForkJoinStateAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static State enterJobSubmit(
@Advice.Argument(value = 0, readOnly = false) ForkJoinTask task) {
if (ExecutorInstrumentationUtils.shouldAttachStateToTask(task)) {
ContextStore<ForkJoinTask, State> contextStore =
InstrumentationContext.get(ForkJoinTask.class, State.class);
return ExecutorInstrumentationUtils.setupState(
contextStore, task, Java8BytecodeBridge.currentContext());
}
return null;
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void exitJobSubmit(
@Advice.Enter State state, @Advice.Thrown Throwable throwable) {
ExecutorInstrumentationUtils.cleanUpOnMethodExit(state, throwable);
}
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.akkaactor;
import static io.opentelemetry.javaagent.tooling.ClassLoaderMatcher.hasClassesNamed;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.extendsClass;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import akka.dispatch.forkjoin.ForkJoinPool;
import akka.dispatch.forkjoin.ForkJoinTask;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.instrumentation.api.ContextStore;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import io.opentelemetry.javaagent.instrumentation.api.concurrent.AdviceUtils;
import io.opentelemetry.javaagent.instrumentation.api.concurrent.State;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.Map;
import java.util.concurrent.Callable;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
/**
* Instrument {@link ForkJoinTask}.
*
* <p>Note: There are quite a few separate implementations of {@code ForkJoinTask}/{@code
* ForkJoinPool}: JVM, Akka, Scala, Netty to name a few. This class handles Akka version.
*/
public class AkkaForkJoinTaskInstrumentation implements TypeInstrumentation {
static final String TASK_CLASS_NAME = "akka.dispatch.forkjoin.ForkJoinTask";
@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed(TASK_CLASS_NAME);
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return extendsClass(named(TASK_CLASS_NAME));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("exec").and(takesArguments(0)).and(not(isAbstract())),
AkkaForkJoinTaskInstrumentation.class.getName() + "$ForkJoinTaskAdvice");
}
public static class ForkJoinTaskAdvice {
/**
* When {@link ForkJoinTask} object is submitted to {@link ForkJoinPool} as {@link Runnable} or
* {@link Callable} it will not get wrapped, instead it will be casted to {@code ForkJoinTask}
* directly. This means state is still stored in {@code Runnable} or {@code Callable} and we
* need to use that state.
*/
@Advice.OnMethodEnter(suppress = Throwable.class)
public static Scope enter(@Advice.This ForkJoinTask thiz) {
ContextStore<ForkJoinTask, State> contextStore =
InstrumentationContext.get(ForkJoinTask.class, State.class);
Scope scope = AdviceUtils.startTaskScope(contextStore, thiz);
if (thiz instanceof Runnable) {
ContextStore<Runnable, State> runnableContextStore =
InstrumentationContext.get(Runnable.class, State.class);
Scope newScope = AdviceUtils.startTaskScope(runnableContextStore, (Runnable) thiz);
if (null != newScope) {
if (null != scope) {
newScope.close();
} else {
scope = newScope;
}
}
}
if (thiz instanceof Callable) {
ContextStore<Callable, State> callableContextStore =
InstrumentationContext.get(Callable.class, State.class);
Scope newScope = AdviceUtils.startTaskScope(callableContextStore, (Callable) thiz);
if (null != newScope) {
if (null != scope) {
newScope.close();
} else {
scope = newScope;
}
}
}
return scope;
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void exit(@Advice.Enter Scope scope) {
if (scope != null) {
scope.close();
}
}
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import io.opentelemetry.instrumentation.test.AgentTestRunner
class AkkaActorTest extends AgentTestRunner {
// TODO this test doesn't really depend on otel.instrumentation.akka-actor.enabled=true
// but setting this property here is needed when running both this test
// and AkkaExecutorInstrumentationTest in the run, otherwise get
// "class redefinition failed: attempted to change superclass or interfaces"
// on whichever test runs second
// (related question is what's the purpose of this test if it doesn't depend on any of the
// instrumentation in this module, is it just to verify that the instrumentation doesn't break
// this scenario?)
static {
System.setProperty("otel.instrumentation.akka-actor.enabled", "true")
}
def "akka #testMethod"() {
setup:
AkkaActors akkaTester = new AkkaActors()
akkaTester."$testMethod"()
expect:
assertTraces(1) {
trace(0, 2) {
span(0) {
name "parent"
attributes {
}
}
span(1) {
name "$expectedGreeting, Akka"
childOf span(0)
attributes {
}
}
}
}
where:
testMethod | expectedGreeting
"basicTell" | "Howdy"
"basicAsk" | "Howdy"
"basicForward" | "Hello"
}
}

View File

@ -0,0 +1,146 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace
import akka.dispatch.forkjoin.ForkJoinPool
import akka.dispatch.forkjoin.ForkJoinTask
import io.opentelemetry.instrumentation.test.AgentTestRunner
import io.opentelemetry.sdk.trace.data.SpanData
import java.lang.reflect.InvocationTargetException
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.Callable
import java.util.concurrent.Future
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import spock.lang.Shared
/**
* Test executor instrumentation for Akka specific classes.
* This is to large extent a copy of ExecutorInstrumentationTest.
*/
class AkkaExecutorInstrumentationTest extends AgentTestRunner {
static {
System.setProperty("otel.instrumentation.akka-actor.enabled", "true")
}
@Shared
def executeRunnable = { e, c -> e.execute((Runnable) c) }
@Shared
def akkaExecuteForkJoinTask = { e, c -> e.execute((ForkJoinTask) c) }
@Shared
def submitRunnable = { e, c -> e.submit((Runnable) c) }
@Shared
def submitCallable = { e, c -> e.submit((Callable) c) }
@Shared
def akkaSubmitForkJoinTask = { e, c -> e.submit((ForkJoinTask) c) }
@Shared
def akkaInvokeForkJoinTask = { e, c -> e.invoke((ForkJoinTask) c) }
def "#poolName '#name' propagates"() {
setup:
def pool = poolImpl
def m = method
new Runnable() {
@Override
void run() {
runUnderTrace("parent") {
// this child will have a span
def child1 = new AkkaAsyncChild()
// this child won't
def child2 = new AkkaAsyncChild(false, false)
m(pool, child1)
m(pool, child2)
child1.waitForCompletion()
child2.waitForCompletion()
}
}
}.run()
TEST_WRITER.waitForTraces(1)
List<SpanData> trace = TEST_WRITER.traces[0]
expect:
TEST_WRITER.traces.size() == 1
trace.size() == 2
trace.get(0).name == "parent"
trace.get(1).name == "asyncChild"
trace.get(1).parentSpanId == trace.get(0).spanId
cleanup:
pool?.shutdown()
// Unfortunately, there's no simple way to test the cross product of methods/pools.
where:
name | method | poolImpl
"execute Runnable" | executeRunnable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue<Runnable>(1))
"submit Runnable" | submitRunnable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue<Runnable>(1))
"submit Callable" | submitCallable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue<Runnable>(1))
// ForkJoinPool has additional set of method overloads for ForkJoinTask to deal with
"execute Runnable" | executeRunnable | new ForkJoinPool()
"execute ForkJoinTask" | akkaExecuteForkJoinTask | new ForkJoinPool()
"submit Runnable" | submitRunnable | new ForkJoinPool()
"submit Callable" | submitCallable | new ForkJoinPool()
"submit ForkJoinTask" | akkaSubmitForkJoinTask | new ForkJoinPool()
"invoke ForkJoinTask" | akkaInvokeForkJoinTask | new ForkJoinPool()
poolName = poolImpl.class.name
}
def "ForkJoinPool '#name' reports after canceled jobs"() {
setup:
def pool = poolImpl
def m = method
List<AkkaAsyncChild> children = new ArrayList<>()
List<Future> jobFutures = new ArrayList<>()
new Runnable() {
@Override
void run() {
runUnderTrace("parent") {
try {
for (int i = 0; i < 20; ++i) {
// Our current instrumentation instrumentation does not behave very well
// if we try to reuse Callable/Runnable. Namely we would be getting 'orphaned'
// child traces sometimes since state can contain only one parent span - and
// we do not really have a good way for attributing work to correct parent span
// if we reuse Callable/Runnable.
// Solution for now is to never reuse a Callable/Runnable.
AkkaAsyncChild child = new AkkaAsyncChild(false, true)
children.add(child)
try {
Future f = m(pool, child)
jobFutures.add(f)
} catch (InvocationTargetException e) {
throw e.getCause()
}
}
} catch (RejectedExecutionException ignored) {
}
for (Future f : jobFutures) {
f.cancel(false)
}
for (AkkaAsyncChild child : children) {
child.unblock()
}
}
}
}.run()
TEST_WRITER.waitForTraces(1)
expect:
TEST_WRITER.traces.size() == 1
where:
name | method | poolImpl
"submit Runnable" | submitRunnable | new ForkJoinPool()
"submit Callable" | submitCallable | new ForkJoinPool()
}
}

View File

@ -0,0 +1,140 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import akka.actor.{Actor, ActorLogging, ActorRef, ActorSystem, Props}
import akka.pattern.ask
import akka.util.Timeout
import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.context.Context
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge
import io.opentelemetry.api.trace.Tracer
import scala.concurrent.duration._
// ! == send-message
object AkkaActors {
val tracer: Tracer =
Java8BytecodeBridge.getGlobalTracer("io.opentelemetry.auto")
val system: ActorSystem = ActorSystem("helloAkka")
val printer: ActorRef = system.actorOf(Receiver.props, "receiverActor")
val howdyGreeter: ActorRef =
system.actorOf(Greeter.props("Howdy", printer), "howdyGreeter")
val forwarder: ActorRef =
system.actorOf(Forwarder.props(printer), "forwarderActor")
val helloGreeter: ActorRef =
system.actorOf(Greeter.props("Hello", forwarder), "helloGreeter")
def tracedChild(opName: String): Unit = {
tracer.spanBuilder(opName).startSpan().end()
}
}
class AkkaActors {
import AkkaActors._
import Greeter._
implicit val timeout: Timeout = 5.minutes
def basicTell(): Unit = {
val parentSpan = tracer.spanBuilder("parent").startSpan()
val parentScope =
Java8BytecodeBridge.currentContext().`with`(parentSpan).makeCurrent()
try {
howdyGreeter ! WhoToGreet("Akka")
howdyGreeter ! Greet
} finally {
parentSpan.end()
parentScope.close()
}
}
def basicAsk(): Unit = {
val parentSpan = tracer.spanBuilder("parent").startSpan()
val parentScope =
Java8BytecodeBridge.currentContext().`with`(parentSpan).makeCurrent()
try {
howdyGreeter ! WhoToGreet("Akka")
howdyGreeter ? Greet
} finally {
parentSpan.end()
parentScope.close()
}
}
def basicForward(): Unit = {
val parentSpan = tracer.spanBuilder("parent").startSpan()
val parentScope =
Java8BytecodeBridge.currentContext().`with`(parentSpan).makeCurrent()
try {
helloGreeter ! WhoToGreet("Akka")
helloGreeter ? Greet
} finally {
parentSpan.end()
parentScope.close()
}
}
}
object Greeter {
def props(message: String, receiverActor: ActorRef): Props =
Props(new Greeter(message, receiverActor))
final case class WhoToGreet(who: String)
case object Greet
}
class Greeter(message: String, receiverActor: ActorRef) extends Actor {
import Greeter._
import Receiver._
var greeting = ""
def receive = {
case WhoToGreet(who) =>
greeting = s"$message, $who"
case Greet =>
receiverActor ! Greeting(greeting)
}
}
object Receiver {
def props: Props = Props[Receiver]
final case class Greeting(greeting: String)
}
class Receiver extends Actor with ActorLogging {
import Receiver._
def receive = {
case Greeting(greeting) => {
AkkaActors.tracedChild(greeting)
}
}
}
object Forwarder {
def props(receiverActor: ActorRef): Props =
Props(new Forwarder(receiverActor))
}
class Forwarder(receiverActor: ActorRef) extends Actor with ActorLogging {
def receive = {
case msg => {
receiverActor forward msg
}
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import akka.dispatch.forkjoin.ForkJoinTask;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
public class AkkaAsyncChild extends ForkJoinTask implements Runnable, Callable {
private static final Tracer tracer = OpenTelemetry.getGlobalTracer("io.opentelemetry.auto");
private final AtomicBoolean blockThread;
private final boolean doTraceableWork;
private final CountDownLatch latch = new CountDownLatch(1);
public AkkaAsyncChild() {
this(true, false);
}
@Override
public Object getRawResult() {
return null;
}
@Override
protected void setRawResult(Object value) {}
@Override
protected boolean exec() {
runImpl();
return true;
}
public AkkaAsyncChild(boolean doTraceableWork, boolean blockThread) {
this.doTraceableWork = doTraceableWork;
this.blockThread = new AtomicBoolean(blockThread);
}
public void unblock() {
blockThread.set(false);
}
@Override
public void run() {
runImpl();
}
@Override
public Object call() {
runImpl();
return null;
}
public void waitForCompletion() throws InterruptedException {
latch.await();
}
private void runImpl() {
while (blockThread.get()) {
// busy-wait to block thread
}
if (doTraceableWork) {
asyncChild();
}
latch.countDown();
}
private void asyncChild() {
tracer.spanBuilder("asyncChild").startSpan().end();
}
}

View File

@ -0,0 +1,70 @@
ext {
// TODO remove once Scala/akka supports Java 15
// https://docs.scala-lang.org/overviews/jdk-compatibility/overview.html
maxJavaVersionForTests = JavaVersion.VERSION_14
}
apply from: "$rootDir/gradle/instrumentation.gradle"
apply from: "$rootDir/gradle/test-with-scala.gradle"
apply plugin: 'org.unbroken-dome.test-sets'
testSets {
version101Test {
dirName = 'test'
}
}
muzzle {
pass {
group = 'com.typesafe.akka'
module = 'akka-http_2.11'
versions = "[10.0.0,10.1.0)"
// later versions of akka-http expect streams to be provided
extraDependency 'com.typesafe.akka:akka-stream_2.11:2.4.14'
}
pass {
group = 'com.typesafe.akka'
module = 'akka-http_2.12'
versions = "[10.0.0,10.1.0)"
// later versions of akka-http expect streams to be provided
extraDependency 'com.typesafe.akka:akka-stream_2.12:2.4.14'
}
pass {
group = 'com.typesafe.akka'
module = 'akka-http_2.11'
versions = "[10.1.0,)"
// later versions of akka-http expect streams to be provided
extraDependency 'com.typesafe.akka:akka-stream_2.11:2.5.11'
}
pass {
group = 'com.typesafe.akka'
module = 'akka-http_2.12'
versions = "[10.1.0,)"
// later versions of akka-http expect streams to be provided
extraDependency 'com.typesafe.akka:akka-stream_2.12:2.5.11'
}
//There is no akka-http 10.0.x series for scala 2.13
pass {
group = 'com.typesafe.akka'
module = 'akka-http_2.13'
versions = "[10.1.8,)"
// later versions of akka-http expect streams to be provided
extraDependency 'com.typesafe.akka:akka-stream_2.13:2.5.23'
}
}
dependencies {
library group: 'com.typesafe.akka', name: 'akka-http_2.11', version: '10.0.0'
library group: 'com.typesafe.akka', name: 'akka-stream_2.11', version: '2.4.14'
// There are some internal API changes in 10.1 that we would like to test separately for
version101TestImplementation group: 'com.typesafe.akka', name: 'akka-http_2.11', version: '10.1.0'
version101TestImplementation group: 'com.typesafe.akka', name: 'akka-stream_2.11', version: '2.5.11'
}
test.dependsOn version101Test
compileVersion101TestGroovy {
classpath = classpath.plus(files(compileVersion101TestScala.destinationDir))
dependsOn compileVersion101TestScala
}

View File

@ -0,0 +1,159 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.akkahttp;
import static io.opentelemetry.javaagent.instrumentation.akkahttp.AkkaHttpClientTracer.tracer;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
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 com.google.auto.service.AutoService;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap.Depth;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.Collections;
import java.util.HashMap;
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 scala.concurrent.Future;
import scala.runtime.AbstractFunction1;
import scala.util.Try;
@AutoService(InstrumentationModule.class)
public class AkkaHttpClientInstrumentationModule extends InstrumentationModule {
public AkkaHttpClientInstrumentationModule() {
super("akka-http", "akka-http-client");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return Collections.singletonList(new HttpExtInstrumentation());
}
public static class HttpExtInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("akka.http.scaladsl.HttpExt");
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
Map<ElementMatcher<? super MethodDescription>, String> transformers = new HashMap<>();
// This is mainly for compatibility with 10.0
transformers.put(
named("singleRequest")
.and(takesArgument(0, named("akka.http.scaladsl.model.HttpRequest"))),
AkkaHttpClientInstrumentationModule.class.getName() + "$SingleRequestAdvice");
// This is for 10.1+
transformers.put(
named("singleRequestImpl")
.and(takesArgument(0, named("akka.http.scaladsl.model.HttpRequest"))),
AkkaHttpClientInstrumentationModule.class.getName() + "$SingleRequestAdvice");
return transformers;
}
}
public static class SingleRequestAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void methodEnter(
@Advice.Argument(value = 0, readOnly = false) HttpRequest request,
@Advice.Local("otelSpan") Span span,
@Advice.Local("otelScope") Scope scope,
@Advice.Local("otelCallDepth") Depth callDepth) {
/*
Versions 10.0 and 10.1 have slightly different structure that is hard to distinguish so here
we cast 'wider net' and avoid instrumenting twice.
In the future we may want to separate these, but since lots of code is reused we would need to come up
with way of continuing to reusing it.
*/
callDepth = tracer().getCallDepth();
if (callDepth.getAndIncrement() == 0) {
span = tracer().startSpan(request);
// Request is immutable, so we have to assign new value once we update headers
AkkaHttpHeaders headers = new AkkaHttpHeaders(request);
scope = tracer().startScope(span, headers);
request = headers.getRequest();
}
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.Argument(0) HttpRequest request,
@Advice.This HttpExt thiz,
@Advice.Return Future<HttpResponse> responseFuture,
@Advice.Thrown Throwable throwable,
@Advice.Local("otelSpan") Span span,
@Advice.Local("otelScope") Scope scope,
@Advice.Local("otelCallDepth") Depth callDepth) {
if (callDepth.decrementAndGet() == 0 && scope != null) {
scope.close();
if (throwable == null) {
responseFuture.onComplete(new OnCompleteHandler(span), thiz.system().dispatcher());
} else {
tracer().endExceptionally(span, throwable);
}
}
}
}
public static class OnCompleteHandler extends AbstractFunction1<Try<HttpResponse>, Void> {
private final Span span;
public OnCompleteHandler(Span span) {
this.span = span;
}
@Override
public Void apply(Try<HttpResponse> result) {
if (result.isSuccess()) {
tracer().end(span, result.get());
} else {
tracer().endExceptionally(span, result.failed().get());
}
return null;
}
}
public static class AkkaHttpHeaders {
private HttpRequest request;
public AkkaHttpHeaders(HttpRequest request) {
this.request = request;
}
public HttpRequest getRequest() {
return request;
}
public void setRequest(HttpRequest request) {
this.request = request;
}
}
public static class InjectAdapter implements TextMapPropagator.Setter<AkkaHttpHeaders> {
public static final InjectAdapter SETTER = new InjectAdapter();
@Override
public void set(AkkaHttpHeaders carrier, String key, String value) {
HttpRequest request = carrier.getRequest();
if (request != null) {
// It looks like this cast is only needed in Java, Scala would have figured it out
carrier.setRequest((HttpRequest) request.addHeader(RawHeader.create(key, value)));
}
}
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.akkahttp;
import static io.opentelemetry.javaagent.instrumentation.akkahttp.AkkaHttpClientInstrumentationModule.InjectAdapter.SETTER;
import akka.http.javadsl.model.HttpHeader;
import akka.http.scaladsl.HttpExt;
import akka.http.scaladsl.model.HttpRequest;
import akka.http.scaladsl.model.HttpResponse;
import io.opentelemetry.context.propagation.TextMapPropagator.Setter;
import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer;
import io.opentelemetry.javaagent.instrumentation.akkahttp.AkkaHttpClientInstrumentationModule.AkkaHttpHeaders;
import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap;
import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap.Depth;
import java.net.URI;
import java.net.URISyntaxException;
public class AkkaHttpClientTracer
extends HttpClientTracer<HttpRequest, AkkaHttpHeaders, HttpResponse> {
private static final AkkaHttpClientTracer TRACER = new AkkaHttpClientTracer();
public static AkkaHttpClientTracer tracer() {
return TRACER;
}
public Depth getCallDepth() {
return CallDepthThreadLocalMap.getCallDepth(HttpExt.class);
}
@Override
protected String method(HttpRequest httpRequest) {
return httpRequest.method().value();
}
@Override
protected URI url(HttpRequest httpRequest) throws URISyntaxException {
return new URI(httpRequest.uri().toString());
}
@Override
protected String flavor(HttpRequest httpRequest) {
return httpRequest.protocol().value();
}
@Override
protected Integer status(HttpResponse httpResponse) {
return httpResponse.status().intValue();
}
@Override
protected String requestHeader(HttpRequest httpRequest, String name) {
return httpRequest.getHeader(name).map(HttpHeader::value).orElse(null);
}
@Override
protected String responseHeader(HttpResponse httpResponse, String name) {
return httpResponse.getHeader(name).map(HttpHeader::value).orElse(null);
}
@Override
protected Setter<AkkaHttpHeaders> getSetter() {
return SETTER;
}
@Override
protected String getInstrumentationName() {
return "io.opentelemetry.javaagent.akka-http";
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.akkahttp;
import akka.http.javadsl.model.HttpHeader;
import akka.http.scaladsl.model.HttpRequest;
import io.opentelemetry.context.propagation.TextMapPropagator;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
public class AkkaHttpServerHeaders implements TextMapPropagator.Getter<HttpRequest> {
public static final AkkaHttpServerHeaders GETTER = new AkkaHttpServerHeaders();
@Override
public Iterable<String> keys(HttpRequest httpRequest) {
return StreamSupport.stream(httpRequest.getHeaders().spliterator(), false)
.map(HttpHeader::lowercaseName)
.collect(Collectors.toList());
}
@Override
public String get(HttpRequest carrier, String key) {
Optional<HttpHeader> header = carrier.getHeader(key);
if (header.isPresent()) {
return header.get().value();
} else {
return null;
}
}
}

View File

@ -0,0 +1,152 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.akkahttp;
import static io.opentelemetry.javaagent.instrumentation.akkahttp.AkkaHttpServerTracer.tracer;
import static java.util.Collections.singletonList;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import akka.http.scaladsl.model.HttpRequest;
import akka.http.scaladsl.model.HttpResponse;
import akka.stream.Materializer;
import com.google.auto.service.AutoService;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.HashMap;
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 scala.Function1;
import scala.concurrent.ExecutionContext;
import scala.concurrent.Future;
import scala.runtime.AbstractFunction1;
@AutoService(InstrumentationModule.class)
public class AkkaHttpServerInstrumentationModule extends InstrumentationModule {
public AkkaHttpServerInstrumentationModule() {
super("akka-http", "akka-http-server");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new HttpExtInstrumentation());
}
public static class HttpExtInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("akka.http.scaladsl.HttpExt");
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
// Instrumenting akka-streams bindAndHandle api was previously attempted.
// This proved difficult as there was no clean way to close the async scope
// in the graph logic after the user's request handler completes.
//
// Instead, we're instrumenting the bindAndHandle function helpers by
// wrapping the scala functions with our own handlers.
Map<ElementMatcher<? super MethodDescription>, String> transformers = new HashMap<>();
transformers.put(
named("bindAndHandleSync").and(takesArgument(0, named("scala.Function1"))),
AkkaHttpServerInstrumentationModule.class.getName() + "$AkkaHttpSyncAdvice");
transformers.put(
named("bindAndHandleAsync").and(takesArgument(0, named("scala.Function1"))),
AkkaHttpServerInstrumentationModule.class.getName() + "$AkkaHttpAsyncAdvice");
return transformers;
}
}
public static class AkkaHttpSyncAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void wrapHandler(
@Advice.Argument(value = 0, readOnly = false)
Function1<HttpRequest, HttpResponse> handler) {
handler = new SyncWrapper(handler);
}
}
public static class AkkaHttpAsyncAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void wrapHandler(
@Advice.Argument(value = 0, readOnly = false)
Function1<HttpRequest, Future<HttpResponse>> handler,
@Advice.Argument(7) Materializer materializer) {
handler = new AsyncWrapper(handler, materializer.executionContext());
}
}
public static class SyncWrapper extends AbstractFunction1<HttpRequest, HttpResponse> {
private final Function1<HttpRequest, HttpResponse> userHandler;
public SyncWrapper(Function1<HttpRequest, HttpResponse> userHandler) {
this.userHandler = userHandler;
}
@Override
public HttpResponse apply(HttpRequest request) {
Context ctx = tracer().startSpan(request, request, "akka.request");
Span span = Java8BytecodeBridge.spanFromContext(ctx);
try (Scope ignored = tracer().startScope(span, null)) {
HttpResponse response = userHandler.apply(request);
tracer().end(span, response);
return response;
} catch (Throwable t) {
tracer().endExceptionally(span, t);
throw t;
}
}
}
public static class AsyncWrapper extends AbstractFunction1<HttpRequest, Future<HttpResponse>> {
private final Function1<HttpRequest, Future<HttpResponse>> userHandler;
private final ExecutionContext executionContext;
public AsyncWrapper(
Function1<HttpRequest, Future<HttpResponse>> userHandler,
ExecutionContext executionContext) {
this.userHandler = userHandler;
this.executionContext = executionContext;
}
@Override
public Future<HttpResponse> apply(HttpRequest request) {
Context ctx = tracer().startSpan(request, request, "akka.request");
Span span = Java8BytecodeBridge.spanFromContext(ctx);
try (Scope ignored = tracer().startScope(span, null)) {
return userHandler
.apply(request)
.transform(
new AbstractFunction1<HttpResponse, HttpResponse>() {
@Override
public HttpResponse apply(HttpResponse response) {
tracer().end(span, response);
return response;
}
},
new AbstractFunction1<Throwable, Throwable>() {
@Override
public Throwable apply(Throwable t) {
tracer().endExceptionally(span, t);
return t;
}
},
executionContext);
} catch (Throwable t) {
tracer().endExceptionally(span, t);
throw t;
}
}
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.akkahttp;
import akka.http.javadsl.model.HttpHeader;
import akka.http.scaladsl.model.HttpRequest;
import akka.http.scaladsl.model.HttpResponse;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapPropagator.Getter;
import io.opentelemetry.instrumentation.api.tracer.HttpServerTracer;
public class AkkaHttpServerTracer
extends HttpServerTracer<HttpRequest, HttpResponse, HttpRequest, Void> {
private static final AkkaHttpServerTracer TRACER = new AkkaHttpServerTracer();
public static AkkaHttpServerTracer tracer() {
return TRACER;
}
@Override
protected String method(HttpRequest httpRequest) {
return httpRequest.method().value();
}
@Override
protected String requestHeader(HttpRequest httpRequest, String name) {
return httpRequest.getHeader(name).map(HttpHeader::value).orElse(null);
}
@Override
protected int responseStatus(HttpResponse httpResponse) {
return httpResponse.status().intValue();
}
@Override
protected void attachServerContext(Context context, Void none) {}
@Override
public Context getServerContext(Void none) {
return null;
}
@Override
protected String url(HttpRequest httpRequest) {
return httpRequest.uri().toString();
}
@Override
protected String peerHostIP(HttpRequest httpRequest) {
return null;
}
@Override
protected String flavor(HttpRequest connection, HttpRequest request) {
return connection.protocol().value();
}
@Override
protected Getter<HttpRequest> getGetter() {
return AkkaHttpServerHeaders.GETTER;
}
@Override
protected String getInstrumentationName() {
return "io.opentelemetry.javaagent.akka-http";
}
@Override
protected Integer peerPort(HttpRequest httpRequest) {
return null;
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import static io.opentelemetry.api.trace.Span.Kind.CLIENT
import akka.actor.ActorSystem
import akka.http.javadsl.Http
import akka.http.javadsl.model.HttpMethods
import akka.http.javadsl.model.HttpRequest
import akka.http.javadsl.model.headers.RawHeader
import akka.stream.ActorMaterializer
import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer
import io.opentelemetry.instrumentation.test.base.HttpClientTest
import spock.lang.Shared
import spock.lang.Timeout
@Timeout(5)
class AkkaHttpClientInstrumentationTest extends HttpClientTest {
@Shared
ActorSystem system = ActorSystem.create()
@Shared
ActorMaterializer materializer = ActorMaterializer.create(system)
@Override
int doRequest(String method, URI uri, Map<String, String> headers, Closure callback) {
def request = HttpRequest.create(uri.toString())
.withMethod(HttpMethods.lookup(method).get())
.addHeaders(headers.collect { RawHeader.create(it.key, it.value) })
def response = Http.get(system)
.singleRequest(request, materializer)
//.whenComplete { result, error ->
// FIXME: Callback should be here instead.
// callback?.call()
//}
.toCompletableFuture()
.get()
callback?.call()
return response.status().intValue()
}
@Override
boolean testRedirects() {
false
}
@Override
boolean testRemoteConnection() {
// Not sure how to properly set timeouts...
return false
}
def "singleRequest exception trace"() {
when:
// Passing null causes NPE in singleRequest
Http.get(system).singleRequest(null, materializer)
then:
thrown NullPointerException
assertTraces(1) {
trace(0, 1) {
span(0) {
hasNoParent()
name HttpClientTracer.DEFAULT_SPAN_NAME
kind CLIENT
errored true
errorEvent(NullPointerException)
}
}
}
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import io.opentelemetry.instrumentation.test.base.HttpServerTest
abstract class AkkaHttpServerInstrumentationTest extends HttpServerTest<Object> {
@Override
boolean testExceptionBody() {
false
}
// FIXME: This doesn't work because we don't support bindAndHandle.
// @Override
// def startServer(int port) {
// AkkaHttpTestWebServer.start(port)
// }
//
// @Override
// void stopServer(Object ignore) {
// AkkaHttpTestWebServer.stop()
// }
@Override
String expectedServerSpanName(ServerEndpoint endpoint) {
return "akka.request"
}
}
class AkkaHttpServerInstrumentationTestSync extends AkkaHttpServerInstrumentationTest {
@Override
def startServer(int port) {
AkkaHttpTestSyncWebServer.start(port)
}
@Override
void stopServer(Object ignore) {
AkkaHttpTestSyncWebServer.stop()
}
}
class AkkaHttpServerInstrumentationTestAsync extends AkkaHttpServerInstrumentationTest {
@Override
def startServer(int port) {
AkkaHttpTestAsyncWebServer.start(port)
}
@Override
void stopServer(Object ignore) {
AkkaHttpTestAsyncWebServer.stop()
}
}

View File

@ -0,0 +1,6 @@
akka.http {
host-connection-pool {
// Limit maximum http backoff for tests
max-connection-backoff = 100ms
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.Http.ServerBinding
import akka.http.scaladsl.model.HttpMethods.GET
import akka.http.scaladsl.model._
import akka.stream.ActorMaterializer
import groovy.lang.Closure
import io.opentelemetry.instrumentation.test.base.HttpServerTest
import io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint._
import scala.concurrent.{Await, ExecutionContextExecutor, Future}
object AkkaHttpTestAsyncWebServer {
implicit val system: ActorSystem = ActorSystem("my-system")
implicit val materializer: ActorMaterializer = ActorMaterializer()
// needed for the future flatMap/onComplete in the end
implicit val executionContext: ExecutionContextExecutor = system.dispatcher
val asyncHandler: HttpRequest => Future[HttpResponse] = {
case HttpRequest(GET, uri: Uri, _, _, _) =>
Future {
val endpoint =
HttpServerTest.ServerEndpoint.forPath(uri.path.toString())
HttpServerTest.controller(
endpoint,
new Closure[HttpResponse](()) {
def doCall(): HttpResponse = {
val resp = HttpResponse(status = endpoint.getStatus) //.withHeaders(headers.Type)resp.contentType = "text/plain"
endpoint match {
case SUCCESS => resp.withEntity(endpoint.getBody)
case QUERY_PARAM => resp.withEntity(uri.queryString().orNull)
case REDIRECT =>
resp.withHeaders(headers.Location(endpoint.getBody))
case ERROR => resp.withEntity(endpoint.getBody)
case EXCEPTION => throw new Exception(endpoint.getBody)
case _ =>
HttpResponse(status = NOT_FOUND.getStatus)
.withEntity(NOT_FOUND.getBody)
}
}
}
)
}
}
private var binding: ServerBinding = _
def start(port: Int): Unit = synchronized {
if (null == binding) {
import scala.concurrent.duration._
binding = Await.result(
Http().bindAndHandleAsync(asyncHandler, "localhost", port),
10 seconds
)
}
}
def stop(): Unit = synchronized {
if (null != binding) {
binding.unbind()
system.terminate()
binding = null
}
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.Http.ServerBinding
import akka.http.scaladsl.model.HttpMethods.GET
import akka.http.scaladsl.model._
import akka.stream.ActorMaterializer
import groovy.lang.Closure
import io.opentelemetry.instrumentation.test.base.HttpServerTest
import io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint._
import scala.concurrent.Await
object AkkaHttpTestSyncWebServer {
implicit val system = ActorSystem("my-system")
implicit val materializer = ActorMaterializer()
// needed for the future flatMap/onComplete in the end
implicit val executionContext = system.dispatcher
val syncHandler: HttpRequest => HttpResponse = {
case HttpRequest(GET, uri: Uri, _, _, _) => {
val endpoint = HttpServerTest.ServerEndpoint.forPath(uri.path.toString())
HttpServerTest.controller(
endpoint,
new Closure[HttpResponse](()) {
def doCall(): HttpResponse = {
val resp = HttpResponse(status = endpoint.getStatus)
endpoint match {
case SUCCESS => resp.withEntity(endpoint.getBody)
case QUERY_PARAM => resp.withEntity(uri.queryString().orNull)
case REDIRECT =>
resp.withHeaders(headers.Location(endpoint.getBody))
case ERROR => resp.withEntity(endpoint.getBody)
case EXCEPTION => throw new Exception(endpoint.getBody)
case _ =>
HttpResponse(status = NOT_FOUND.getStatus)
.withEntity(NOT_FOUND.getBody)
}
}
}
)
}
}
private var binding: ServerBinding = null
def start(port: Int): Unit = synchronized {
if (null == binding) {
import scala.concurrent.duration._
binding = Await.result(
Http().bindAndHandleSync(syncHandler, "localhost", port),
10 seconds
)
}
}
def stop(): Unit = synchronized {
if (null != binding) {
binding.unbind()
system.terminate()
binding = null
}
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.Http.ServerBinding
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.ExceptionHandler
import akka.stream.ActorMaterializer
import io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint._
import scala.concurrent.Await
// FIXME: This doesn't work because we don't support bindAndHandle.
object AkkaHttpTestWebServer {
implicit val system = ActorSystem("my-system")
implicit val materializer = ActorMaterializer()
// needed for the future flatMap/onComplete in the end
implicit val executionContext = system.dispatcher
val exceptionHandler = ExceptionHandler {
case ex: Exception =>
complete(
HttpResponse(status = EXCEPTION.getStatus).withEntity(ex.getMessage)
)
}
val route = { //handleExceptions(exceptionHandler) {
path(SUCCESS.rawPath) {
complete(
HttpResponse(status = SUCCESS.getStatus).withEntity(SUCCESS.getBody)
)
} ~ path(QUERY_PARAM.rawPath) {
complete(
HttpResponse(status = QUERY_PARAM.getStatus).withEntity(SUCCESS.getBody)
)
} ~ path(REDIRECT.rawPath) {
redirect(Uri(REDIRECT.getBody), StatusCodes.Found)
} ~ path(ERROR.rawPath) {
complete(HttpResponse(status = ERROR.getStatus).withEntity(ERROR.getBody))
} ~ path(EXCEPTION.rawPath) {
failWith(new Exception(EXCEPTION.getBody))
}
}
private var binding: ServerBinding = null
def start(port: Int): Unit = synchronized {
if (null == binding) {
import scala.concurrent.duration._
binding =
Await.result(Http().bindAndHandle(route, "localhost", port), 10 seconds)
}
}
def stop(): Unit = synchronized {
if (null != binding) {
binding.unbind()
system.terminate()
binding = null
}
}
}

View File

@ -22,7 +22,7 @@ dependencies {
testLibrary group: 'org.apache.camel', name: 'camel-undertow', version: '2.20.1'
testImplementation project(':instrumentation:apache-httpclient:apache-httpclient-2.0')
testImplementation project(':instrumentation:servlet:servlet-3.0')
testImplementation project(':instrumentation:servlet:servlet-3.0:javaagent')
testImplementation group: 'org.spockframework', name: 'spock-spring', version: "$versions.spock"

View File

@ -0,0 +1,40 @@
ext {
minJavaVersionForTests = JavaVersion.VERSION_1_8
}
apply from: "$rootDir/gradle/instrumentation.gradle"
muzzle {
pass {
group = "org.apache.camel"
module = "camel-core"
versions = "[2.20.1,3)"
}
}
dependencies {
library group: 'org.apache.camel', name: 'camel-core', version: '2.20.1'
testLibrary group: 'org.apache.camel', name: 'camel-spring-boot-starter', version: '2.20.1'
testLibrary group: 'org.apache.camel', name: 'camel-jetty-starter', version: '2.20.1'
testLibrary group: 'org.apache.camel', name: 'camel-http-starter', version: '2.20.1'
testLibrary group: 'org.apache.camel', name: 'camel-jaxb-starter', version: '2.20.1'
testLibrary group: 'org.apache.camel', name: 'camel-undertow', version: '2.20.1'
testImplementation project(':instrumentation:apache-httpclient:apache-httpclient-2.0:javaagent')
testImplementation project(':instrumentation:servlet:servlet-3.0:javaagent')
testImplementation group: 'org.spockframework', name: 'spock-spring', version: "$versions.spock"
testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '1.5.17.RELEASE'
testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter', version: '1.5.17.RELEASE'
testImplementation 'javax.xml.bind:jaxb-api:2.3.1'
latestDepTestLibrary group: 'org.apache.camel', name: 'camel-core', version: '2.+'
latestDepTestLibrary group: 'org.apache.camel', name: 'camel-spring-boot-starter', version: '2.+'
latestDepTestLibrary group: 'org.apache.camel', name: 'camel-jetty-starter', version: '2.+'
latestDepTestLibrary group: 'org.apache.camel', name: 'camel-http-starter', version: '2.+'
latestDepTestLibrary group: 'org.apache.camel', name: 'camel-jaxb-starter', version: '2.+'
latestDepTestLibrary group: 'org.apache.camel', name: 'camel-undertow', version: '2.+'
}

View File

@ -0,0 +1,121 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Apache Camel Opentracing Component
*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apachecamel;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
import org.apache.camel.Exchange;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Utility class for managing active spans as a stack associated with an exchange. */
class ActiveSpanManager {
private static final String ACTIVE_SPAN_PROPERTY = "OpenTelemetry.activeSpan";
private static final Logger LOG = LoggerFactory.getLogger(ActiveSpanManager.class);
private ActiveSpanManager() {}
public static Span getSpan(Exchange exchange) {
SpanWithScope spanWithScope = exchange.getProperty(ACTIVE_SPAN_PROPERTY, SpanWithScope.class);
if (spanWithScope != null) {
return spanWithScope.getSpan();
}
return null;
}
/**
* This method activates the supplied span for the supplied exchange. If an existing span is found
* for the exchange, this will be pushed onto a stack.
*
* @param exchange The exchange
* @param span The span
*/
public static void activate(Exchange exchange, Span span) {
SpanWithScope parent = exchange.getProperty(ACTIVE_SPAN_PROPERTY, SpanWithScope.class);
SpanWithScope spanWithScope = SpanWithScope.activate(span, parent);
exchange.setProperty(ACTIVE_SPAN_PROPERTY, spanWithScope);
if (LOG.isTraceEnabled()) {
LOG.trace("Activated a span: " + spanWithScope);
}
}
/**
* This method deactivates an existing active span associated with the supplied exchange. Once
* deactivated, if a parent span is found associated with the stack for the exchange, it will be
* restored as the current span for that exchange.
*
* @param exchange The exchange
*/
public static void deactivate(Exchange exchange) {
SpanWithScope spanWithScope = exchange.getProperty(ACTIVE_SPAN_PROPERTY, SpanWithScope.class);
if (spanWithScope != null) {
spanWithScope.deactivate();
exchange.setProperty(ACTIVE_SPAN_PROPERTY, spanWithScope.getParent());
if (LOG.isTraceEnabled()) {
LOG.trace("Deactivated span: " + spanWithScope);
}
}
}
public static class SpanWithScope {
@Nullable private final SpanWithScope parent;
private final Span span;
private final Scope scope;
public SpanWithScope(SpanWithScope parent, Span span, Scope scope) {
this.parent = parent;
this.span = span;
this.scope = scope;
}
public static SpanWithScope activate(Span span, SpanWithScope parent) {
Scope scope = CamelTracer.TRACER.startScope(span);
return new SpanWithScope(parent, span, scope);
}
public SpanWithScope getParent() {
return parent;
}
public Span getSpan() {
return span;
}
public void deactivate() {
span.end();
scope.close();
}
@Override
public String toString() {
return "SpanWithScope [span=" + span + ", scope=" + scope + "]";
}
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachecamel;
import static io.opentelemetry.javaagent.tooling.ClassLoaderMatcher.hasClassesNamed;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface;
import static java.util.Collections.singletonList;
import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
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.camel.CamelContext;
@AutoService(InstrumentationModule.class)
public class ApacheCamelInstrumentationModule extends InstrumentationModule {
public ApacheCamelInstrumentationModule() {
super("apache-camel", "apache-camel-2.20");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new CamelContextInstrumentation());
}
public static class CamelContextInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("org.apache.camel.CamelContext");
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return not(isAbstract()).and(implementsInterface(named("org.apache.camel.CamelContext")));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return Collections.singletonMap(
named("start").and(isPublic()).and(takesArguments(0)),
ApacheCamelInstrumentationModule.class.getName() + "$ContextAdvice");
}
}
public static class ContextAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onContextStart(@Advice.This final CamelContext context) throws Exception {
if (context.hasService(CamelTracingService.class) == null) {
// start this service eager so we init before Camel is starting up
context.addService(new CamelTracingService(context), true, true);
}
}
}
}

View File

@ -0,0 +1,11 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachecamel;
public enum CamelDirection {
INBOUND,
OUTBOUND;
}

View File

@ -0,0 +1,101 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Apache Camel Opentracing Component
*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apachecamel;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import java.util.EventObject;
import org.apache.camel.management.event.ExchangeSendingEvent;
import org.apache.camel.management.event.ExchangeSentEvent;
import org.apache.camel.support.EventNotifierSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
final class CamelEventNotifier extends EventNotifierSupport {
private static final Logger LOG = LoggerFactory.getLogger(CamelEventNotifier.class);
@Override
public void notify(EventObject event) {
try {
if (event instanceof ExchangeSendingEvent) {
onExchangeSending((ExchangeSendingEvent) event);
} else if (event instanceof ExchangeSentEvent) {
onExchangeSent((ExchangeSentEvent) event);
}
} catch (Throwable t) {
LOG.warn("Failed to capture tracing data", t);
}
}
/** Camel about to send (outbound). */
private void onExchangeSending(ExchangeSendingEvent ese) {
SpanDecorator sd = CamelTracer.TRACER.getSpanDecorator(ese.getEndpoint());
if (!sd.shouldStartNewSpan()) {
return;
}
String name =
sd.getOperationName(ese.getExchange(), ese.getEndpoint(), CamelDirection.OUTBOUND);
Span span = CamelTracer.TRACER.startSpan(name, sd.getInitiatorSpanKind());
sd.pre(span, ese.getExchange(), ese.getEndpoint(), CamelDirection.OUTBOUND);
CamelPropagationUtil.injectParent(Context.current(), ese.getExchange().getIn().getHeaders());
ActiveSpanManager.activate(ese.getExchange(), span);
if (LOG.isTraceEnabled()) {
LOG.trace("[Exchange sending] Initiator span started " + span);
}
}
/** Camel finished sending (outbound). Finish span and remove it from CAMEL holder. */
private void onExchangeSent(ExchangeSentEvent event) {
ExchangeSentEvent ese = event;
SpanDecorator sd = CamelTracer.TRACER.getSpanDecorator(ese.getEndpoint());
if (!sd.shouldStartNewSpan()) {
return;
}
Span span = ActiveSpanManager.getSpan(ese.getExchange());
if (span != null) {
if (LOG.isTraceEnabled()) {
LOG.trace("[Exchange sent] Initiator span finished " + span);
}
sd.post(span, ese.getExchange(), ese.getEndpoint());
ActiveSpanManager.deactivate(ese.getExchange());
} else {
LOG.warn("Could not find managed span for exchange " + ese.getExchange());
}
}
@Override
public boolean isEnabled(EventObject event) {
return event instanceof ExchangeSendingEvent || event instanceof ExchangeSentEvent;
}
@Override
public String toString() {
return "OpenTelemetryCamelEventNotifier";
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachecamel;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapPropagator.Getter;
import io.opentelemetry.context.propagation.TextMapPropagator.Setter;
import java.util.Map;
final class CamelPropagationUtil {
private CamelPropagationUtil() {}
static Context extractParent(final Map<String, Object> exchangeHeaders) {
return OpenTelemetry.getGlobalPropagators()
.getTextMapPropagator()
.extract(Context.current(), exchangeHeaders, MapGetter.INSTANCE);
}
static void injectParent(Context context, final Map<String, Object> exchangeHeaders) {
OpenTelemetry.getGlobalPropagators()
.getTextMapPropagator()
.inject(context, exchangeHeaders, MapSetter.INSTANCE);
}
private static class MapGetter implements Getter<Map<String, Object>> {
private static final MapGetter INSTANCE = new MapGetter();
@Override
public Iterable<String> keys(Map<String, Object> map) {
return map.keySet();
}
@Override
public String get(Map<String, Object> map, String s) {
return (map.containsKey(s) ? map.get(s).toString() : null);
}
}
private static class MapSetter implements Setter<Map<String, Object>> {
private static final MapSetter INSTANCE = new MapSetter();
@Override
public void set(Map<String, Object> carrier, String key, String value) {
// Camel keys are internal ones
if (!key.startsWith("Camel")) {
carrier.put(key, value);
}
}
}
}

View File

@ -0,0 +1,92 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Apache Camel Opentracing Component
*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apachecamel;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.context.Context;
import org.apache.camel.Exchange;
import org.apache.camel.Route;
import org.apache.camel.support.RoutePolicySupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
final class CamelRoutePolicy extends RoutePolicySupport {
private static final Logger LOG = LoggerFactory.getLogger(CamelRoutePolicy.class);
private Span spanOnExchangeBegin(Route route, Exchange exchange, SpanDecorator sd) {
Span activeSpan = CamelTracer.TRACER.getCurrentSpan();
String name = sd.getOperationName(exchange, route.getEndpoint(), CamelDirection.INBOUND);
SpanBuilder builder = CamelTracer.TRACER.spanBuilder(name);
if (!activeSpan.getSpanContext().isValid()) {
// root operation, set kind, otherwise - INTERNAL
builder.setSpanKind(sd.getReceiverSpanKind());
Context parentContext = CamelPropagationUtil.extractParent(exchange.getIn().getHeaders());
if (parentContext != null) {
builder.setParent(parentContext);
}
}
return builder.startSpan();
}
/**
* Route exchange started, ie request could have been already captured by upper layer
* instrumentation.
*/
@Override
public void onExchangeBegin(Route route, Exchange exchange) {
try {
SpanDecorator sd = CamelTracer.TRACER.getSpanDecorator(route.getEndpoint());
Span span = spanOnExchangeBegin(route, exchange, sd);
sd.pre(span, exchange, route.getEndpoint(), CamelDirection.INBOUND);
ActiveSpanManager.activate(exchange, span);
if (LOG.isTraceEnabled()) {
LOG.trace("[Route start] Receiver span started " + span);
}
} catch (Throwable t) {
LOG.warn("Failed to capture tracing data", t);
}
}
/** Route exchange done. Get active CAMEL span, finish, remove from CAMEL holder. */
@Override
public void onExchangeDone(Route route, Exchange exchange) {
try {
Span span = ActiveSpanManager.getSpan(exchange);
if (span != null) {
if (LOG.isTraceEnabled()) {
LOG.trace("[Route finished] Receiver span finished " + span);
}
SpanDecorator sd = CamelTracer.TRACER.getSpanDecorator(route.getEndpoint());
sd.post(span, exchange, route.getEndpoint());
ActiveSpanManager.deactivate(exchange);
} else {
LOG.warn("Could not find managed span for exchange=" + exchange);
}
} catch (Throwable t) {
LOG.warn("Failed to capture tracing data", t);
}
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Apache Camel Opentracing Component
*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apachecamel;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.instrumentation.api.tracer.BaseTracer;
import io.opentelemetry.javaagent.instrumentation.apachecamel.decorators.DecoratorRegistry;
import org.apache.camel.Endpoint;
import org.apache.camel.util.StringHelper;
class CamelTracer extends BaseTracer {
public static final CamelTracer TRACER = new CamelTracer();
private final DecoratorRegistry registry = new DecoratorRegistry();
@Override
protected String getInstrumentationName() {
return "io.opentelemetry.javaagent.apache-camel";
}
public SpanBuilder spanBuilder(String name) {
return tracer.spanBuilder(name);
}
public SpanDecorator getSpanDecorator(Endpoint endpoint) {
String component = "";
String uri = endpoint.getEndpointUri();
String[] splitUri = StringHelper.splitOnCharacter(uri, ":", 2);
if (splitUri[1] != null) {
component = splitUri[0];
}
return registry.forComponent(component);
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Apache Camel Opentracing Component
*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apachecamel;
import org.apache.camel.CamelContext;
import org.apache.camel.StaticService;
import org.apache.camel.model.RouteDefinition;
import org.apache.camel.spi.RoutePolicy;
import org.apache.camel.spi.RoutePolicyFactory;
import org.apache.camel.support.ServiceSupport;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.ServiceHelper;
public class CamelTracingService extends ServiceSupport
implements RoutePolicyFactory, StaticService {
private final CamelContext camelContext;
private final CamelEventNotifier eventNotifier = new CamelEventNotifier();
private final CamelRoutePolicy routePolicy = new CamelRoutePolicy();
public CamelTracingService(CamelContext camelContext) {
ObjectHelper.notNull(camelContext, "CamelContext", this);
this.camelContext = camelContext;
}
@Override
protected void doStart() throws Exception {
camelContext.getManagementStrategy().addEventNotifier(eventNotifier);
if (!camelContext.getRoutePolicyFactories().contains(this)) {
camelContext.addRoutePolicyFactory(this);
}
ServiceHelper.startServices(eventNotifier);
}
@Override
protected void doStop() throws Exception {
// stop event notifier
camelContext.getManagementStrategy().removeEventNotifier(eventNotifier);
ServiceHelper.stopService(eventNotifier);
// remove route policy
camelContext.getRoutePolicyFactories().remove(this);
}
@Override
public RoutePolicy createRoutePolicy(
CamelContext camelContext, String routeId, RouteDefinition route) {
return routePolicy;
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Apache Camel Opentracing Component
*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apachecamel;
import io.opentelemetry.api.trace.Span;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
/** This interface represents a decorator specific to the component/endpoint being instrumented. */
public interface SpanDecorator {
/**
* This method indicates whether the component associated with the SpanDecorator should result in
* a new span being created.
*
* @return Whether a new span should be created
*/
boolean shouldStartNewSpan();
/**
* Returns the operation name to use with the Span representing this exchange and endpoint.
*
* @param exchange The exchange
* @param endpoint The endpoint
* @return The operation name
*/
String getOperationName(Exchange exchange, Endpoint endpoint, CamelDirection camelDirection);
/**
* This method adds appropriate details (tags/logs) to the supplied span based on the pre
* processing of the exchange.
*
* @param span The span
* @param exchange The exchange
* @param endpoint The endpoint
*/
void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection);
/**
* This method adds appropriate details (tags/logs) to the supplied span based on the post
* processing of the exchange.
*
* @param span The span
* @param exchange The exchange
* @param endpoint The endpoint
*/
void post(Span span, Exchange exchange, Endpoint endpoint);
/** Returns the 'span.kind' value for use when the component is initiating a communication. */
Span.Kind getInitiatorSpanKind();
/** Returns the 'span.kind' value for use when the component is receiving a communication. */
Span.Kind getReceiverSpanKind();
}

View File

@ -0,0 +1,117 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Apache Camel Opentracing Component
*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apachecamel.decorators;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Span.Kind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection;
import io.opentelemetry.javaagent.instrumentation.apachecamel.SpanDecorator;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
import org.apache.camel.util.StringHelper;
import org.apache.camel.util.URISupport;
/** An abstract base implementation of the {@link SpanDecorator} interface. */
class BaseSpanDecorator implements SpanDecorator {
static final String DEFAULT_OPERATION_NAME = "CamelOperation";
/**
* This method removes the scheme, any leading slash characters and options from the supplied URI.
* This is intended to extract a meaningful name from the URI that can be used in situations, such
* as the operation name.
*
* @param endpoint The endpoint
* @return The stripped value from the URI
*/
public static String stripSchemeAndOptions(Endpoint endpoint) {
int start = endpoint.getEndpointUri().indexOf(':');
start++;
// Remove any leading '/'
while (endpoint.getEndpointUri().charAt(start) == '/') {
start++;
}
int end = endpoint.getEndpointUri().indexOf('?');
return end == -1
? endpoint.getEndpointUri().substring(start)
: endpoint.getEndpointUri().substring(start, end);
}
public static Map<String, String> toQueryParameters(String uri) {
int index = uri.indexOf('?');
if (index != -1) {
String queryString = uri.substring(index + 1);
Map<String, String> map = new HashMap<>();
for (String param : queryString.split("&")) {
String[] parts = param.split("=");
if (parts.length == 2) {
map.put(parts[0], parts[1]);
}
}
return map;
}
return Collections.emptyMap();
}
@Override
public boolean shouldStartNewSpan() {
return true;
}
@Override
public String getOperationName(
Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) {
String[] splitUri = StringHelper.splitOnCharacter(endpoint.getEndpointUri(), ":", 2);
return (splitUri.length > 0 ? splitUri[0] : DEFAULT_OPERATION_NAME);
}
@Override
public void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) {
span.setAttribute("camel.uri", URISupport.sanitizeUri(endpoint.getEndpointUri()));
}
@Override
public void post(Span span, Exchange exchange, Endpoint endpoint) {
if (exchange.isFailed()) {
span.setStatus(StatusCode.ERROR);
if (exchange.getException() != null) {
span.recordException(exchange.getException());
}
}
}
@Override
public Span.Kind getInitiatorSpanKind() {
return Kind.CLIENT;
}
@Override
public Span.Kind getReceiverSpanKind() {
return Kind.SERVER;
}
}

View File

@ -0,0 +1,130 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Apache Camel Opentracing Component
*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apachecamel.decorators;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.attributes.SemanticAttributes;
import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection;
import java.net.URI;
import java.util.Map;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
class DbSpanDecorator extends BaseSpanDecorator {
private final String component;
private final String system;
DbSpanDecorator(String component, String system) {
this.component = component;
this.system = system;
}
@Override
public String getOperationName(
Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) {
switch (component) {
case "mongodb":
case "elasticsearch":
Map<String, String> queryParameters = toQueryParameters(endpoint.getEndpointUri());
if (queryParameters.containsKey("operation")) {
return queryParameters.get("operation");
}
return super.getOperationName(exchange, endpoint, camelDirection);
default:
return super.getOperationName(exchange, endpoint, camelDirection);
}
}
private String getStatement(Exchange exchange, Endpoint endpoint) {
switch (component) {
case "mongodb":
Map<String, String> mongoParameters = toQueryParameters(endpoint.getEndpointUri());
return mongoParameters.toString();
case "cql":
Object cqlObj = exchange.getIn().getHeader("CamelCqlQuery");
if (cqlObj != null) {
return cqlObj.toString();
}
Map<String, String> cqlParameters = toQueryParameters(endpoint.getEndpointUri());
if (cqlParameters.containsKey("cql")) {
return cqlParameters.get("cql");
}
return null;
case "jdbc":
Object body = exchange.getIn().getBody();
if (body instanceof String) {
return (String) body;
}
return null;
case "sql":
Object sqlquery = exchange.getIn().getHeader("CamelSqlQuery");
if (sqlquery instanceof String) {
return (String) sqlquery;
}
return null;
default:
return null;
}
}
private String getDbName(Endpoint endpoint) {
switch (component) {
case "mongodb":
Map<String, String> mongoParameters = toQueryParameters(endpoint.getEndpointUri());
return mongoParameters.get("database");
case "cql":
URI uri = URI.create(endpoint.getEndpointUri());
if (uri.getPath() != null && uri.getPath().length() > 0) {
// Strip leading '/' from path
return uri.getPath().substring(1);
}
return null;
case "elasticsearch":
Map<String, String> elasticsearchParameters = toQueryParameters(endpoint.getEndpointUri());
if (elasticsearchParameters.containsKey("indexName")) {
return elasticsearchParameters.get("indexName");
}
return null;
default:
return null;
}
}
@Override
public void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) {
super.pre(span, exchange, endpoint, camelDirection);
span.setAttribute(SemanticAttributes.DB_SYSTEM, system);
String statement = getStatement(exchange, endpoint);
if (statement != null) {
span.setAttribute(SemanticAttributes.DB_STATEMENT, statement);
}
String dbName = getDbName(endpoint);
if (dbName != null) {
span.setAttribute(SemanticAttributes.DB_NAME, dbName);
}
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators;
import io.opentelemetry.javaagent.instrumentation.apachecamel.SpanDecorator;
import io.opentelemetry.javaagent.instrumentation.api.db.DbSystem;
import java.util.HashMap;
import java.util.Map;
public class DecoratorRegistry {
private static final SpanDecorator DEFAULT = new BaseSpanDecorator();
private static final Map<String, SpanDecorator> DECORATORS = loadDecorators();
private static Map<String, SpanDecorator> loadDecorators() {
Map<String, SpanDecorator> result = new HashMap<>();
result.put("ahc", new HttpSpanDecorator());
result.put("ampq", new MessagingSpanDecorator("ampq"));
result.put("aws-sns", new MessagingSpanDecorator("aws-sns"));
result.put("aws-sqs", new MessagingSpanDecorator("aws-sqs"));
result.put("cometd", new MessagingSpanDecorator("cometd"));
result.put("cometds", new MessagingSpanDecorator("cometds"));
result.put("cql", new DbSpanDecorator("cql", DbSystem.CASSANDRA));
result.put("direct", new InternalSpanDecorator());
result.put("direct-vm", new InternalSpanDecorator());
result.put("disruptor", new InternalSpanDecorator());
result.put("disruptor-vm", new InternalSpanDecorator());
result.put("elasticsearch", new DbSpanDecorator("elasticsearch", "elasticsearch"));
result.put("http4", new HttpSpanDecorator());
result.put("http", new HttpSpanDecorator());
result.put("ironmq", new MessagingSpanDecorator("ironmq"));
result.put("jdbc", new DbSpanDecorator("jdbc", DbSystem.OTHER_SQL));
result.put("jetty", new HttpSpanDecorator());
result.put("jms", new MessagingSpanDecorator("jms"));
result.put("kafka", new KafkaSpanDecorator());
result.put("log", new LogSpanDecorator());
result.put("mongodb", new DbSpanDecorator("mongodb", DbSystem.MONGODB));
result.put("mqtt", new MessagingSpanDecorator("mqtt"));
result.put("netty-http4", new HttpSpanDecorator());
result.put("netty-http", new HttpSpanDecorator());
result.put("paho", new MessagingSpanDecorator("paho"));
result.put("rabbitmq", new MessagingSpanDecorator("rabbitmq"));
result.put("restlet", new HttpSpanDecorator());
result.put("rest", new RestSpanDecorator());
result.put("seda", new InternalSpanDecorator());
result.put("servlet", new HttpSpanDecorator());
result.put("sjms", new MessagingSpanDecorator("sjms"));
result.put("sql", new DbSpanDecorator("sql", DbSystem.OTHER_SQL));
result.put("stomp", new MessagingSpanDecorator("stomp"));
result.put("timer", new TimerSpanDecorator());
result.put("undertow", new HttpSpanDecorator());
result.put("vm", new InternalSpanDecorator());
return result;
}
public SpanDecorator forComponent(final String component) {
return DECORATORS.getOrDefault(component, DEFAULT);
}
}

View File

@ -0,0 +1,153 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Apache Camel Opentracing Component
*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apachecamel.decorators;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.attributes.SemanticAttributes;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.tracer.BaseTracer;
import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection;
import java.net.MalformedURLException;
import java.net.URL;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
import org.checkerframework.checker.nullness.qual.Nullable;
class HttpSpanDecorator extends BaseSpanDecorator {
private static final String POST_METHOD = "POST";
private static final String GET_METHOD = "GET";
protected static String getHttpMethod(Exchange exchange, Endpoint endpoint) {
// 1. Use method provided in header.
Object method = exchange.getIn().getHeader(Exchange.HTTP_METHOD);
if (method instanceof String) {
return (String) method;
}
// 2. GET if query string is provided in header.
if (exchange.getIn().getHeader(Exchange.HTTP_QUERY) != null) {
return GET_METHOD;
}
// 3. GET if endpoint is configured with a query string.
if (endpoint.getEndpointUri().indexOf('?') != -1) {
return GET_METHOD;
}
// 4. POST if there is data to send (body is not null).
if (exchange.getIn().getBody() != null) {
return POST_METHOD;
}
// 5. GET otherwise.
return GET_METHOD;
}
@Override
public String getOperationName(
Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) {
// Based on HTTP component documentation:
String spanName = null;
if (shouldSetPathAsName(camelDirection)) {
spanName = getPath(exchange, endpoint);
}
return (spanName == null ? getHttpMethod(exchange, endpoint) : spanName);
}
@Override
public void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) {
super.pre(span, exchange, endpoint, camelDirection);
String httpUrl = getHttpUrl(exchange, endpoint);
if (httpUrl != null) {
span.setAttribute(SemanticAttributes.HTTP_URL, httpUrl);
}
span.setAttribute(SemanticAttributes.HTTP_METHOD, getHttpMethod(exchange, endpoint));
Span serverSpan = Context.current().get(BaseTracer.CONTEXT_SERVER_SPAN_KEY);
if (shouldUpdateServerSpanName(serverSpan, camelDirection)) {
updateServerSpanName(serverSpan, exchange, endpoint);
}
}
private boolean shouldSetPathAsName(CamelDirection camelDirection) {
return CamelDirection.INBOUND.equals(camelDirection);
}
@Nullable
protected String getPath(Exchange exchange, Endpoint endpoint) {
String httpUrl = getHttpUrl(exchange, endpoint);
try {
URL url = new URL(httpUrl);
return url.getPath();
} catch (MalformedURLException e) {
return null;
}
}
private boolean shouldUpdateServerSpanName(Span serverSpan, CamelDirection camelDirection) {
return (serverSpan != null && shouldSetPathAsName(camelDirection));
}
private void updateServerSpanName(Span serverSpan, Exchange exchange, Endpoint endpoint) {
String path = getPath(exchange, endpoint);
if (path != null) {
serverSpan.updateName(path);
}
}
protected String getHttpUrl(Exchange exchange, Endpoint endpoint) {
Object url = exchange.getIn().getHeader(Exchange.HTTP_URL);
if (url instanceof String) {
return (String) url;
} else {
Object uri = exchange.getIn().getHeader(Exchange.HTTP_URI);
if (uri instanceof String) {
return (String) uri;
} else {
// Try to obtain from endpoint
int index = endpoint.getEndpointUri().lastIndexOf("http:");
if (index != -1) {
return endpoint.getEndpointUri().substring(index);
}
}
}
return null;
}
@Override
public void post(Span span, Exchange exchange, Endpoint endpoint) {
super.post(span, exchange, endpoint);
if (exchange.hasOut()) {
Object responseCode = exchange.getOut().getHeader(Exchange.HTTP_RESPONSE_CODE);
if (responseCode instanceof Integer) {
span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, (Integer) responseCode);
}
}
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Apache Camel Opentracing Component
*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apachecamel.decorators;
import io.opentelemetry.api.trace.Span.Kind;
import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
class InternalSpanDecorator extends BaseSpanDecorator {
@Override
public String getOperationName(
Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) {
// Internal communications use descriptive names, so suitable
// as an operation name, but need to strip the scheme and any options
return stripSchemeAndOptions(endpoint);
}
@Override
public boolean shouldStartNewSpan() {
return false;
}
@Override
public Kind getReceiverSpanKind() {
return Kind.INTERNAL;
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Apache Camel Opentracing Component
*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apachecamel.decorators;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.attributes.SemanticAttributes;
import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection;
import java.util.Map;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
class KafkaSpanDecorator extends MessagingSpanDecorator {
private static final String PARTITION_KEY = "kafka.PARTITION_KEY";
private static final String PARTITION = "kafka.PARTITION";
private static final String KEY = "kafka.KEY";
private static final String TOPIC = "kafka.TOPIC";
private static final String OFFSET = "kafka.OFFSET";
public KafkaSpanDecorator() {
super("kafka");
}
@Override
public String getDestination(Exchange exchange, Endpoint endpoint) {
String topic = (String) exchange.getIn().getHeader(TOPIC);
if (topic == null) {
Map<String, String> queryParameters = toQueryParameters(endpoint.getEndpointUri());
topic = queryParameters.get("topic");
}
return topic != null ? topic : super.getDestination(exchange, endpoint);
}
@Override
public void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) {
super.pre(span, exchange, endpoint, camelDirection);
span.setAttribute(SemanticAttributes.MESSAGING_OPERATION, "process");
span.setAttribute(SemanticAttributes.MESSAGING_DESTINATION_KIND, "topic");
String partition = getValue(exchange, PARTITION, Integer.class);
if (partition != null) {
span.setAttribute("partition", partition);
}
String partitionKey = (String) exchange.getIn().getHeader(PARTITION_KEY);
if (partitionKey != null) {
span.setAttribute("partitionKey", partitionKey);
}
String key = (String) exchange.getIn().getHeader(KEY);
if (key != null) {
span.setAttribute("key", key);
}
String offset = getValue(exchange, OFFSET, Long.class);
if (offset != null) {
span.setAttribute("offset", offset);
}
}
/**
* Extracts header value from the exchange for given header.
*
* @param exchange the {@link Exchange}
* @param header the header name
* @param type the class type of the exchange header
*/
private <T> String getValue(final Exchange exchange, final String header, Class<T> type) {
T value = exchange.getIn().getHeader(header, type);
return value != null ? String.valueOf(value) : exchange.getIn().getHeader(header, String.class);
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Apache Camel Opentracing Component
*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apachecamel.decorators;
class LogSpanDecorator extends BaseSpanDecorator {
@Override
public boolean shouldStartNewSpan() {
return false;
}
}

View File

@ -0,0 +1,124 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Apache Camel Opentracing Component
*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apachecamel.decorators;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Span.Kind;
import io.opentelemetry.api.trace.attributes.SemanticAttributes;
import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection;
import java.net.URI;
import java.util.Map;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
class MessagingSpanDecorator extends BaseSpanDecorator {
private final String component;
public MessagingSpanDecorator(String component) {
this.component = component;
}
@Override
public String getOperationName(
Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) {
if ("mqtt".equals(component)) {
return stripSchemeAndOptions(endpoint);
}
return getDestination(exchange, endpoint);
}
@Override
public void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) {
super.pre(span, exchange, endpoint, camelDirection);
span.setAttribute(SemanticAttributes.MESSAGING_DESTINATION, getDestination(exchange, endpoint));
String messageId = getMessageId(exchange);
if (messageId != null) {
span.setAttribute(SemanticAttributes.MESSAGING_MESSAGE_ID, messageId);
}
}
/**
* This method identifies the destination from the supplied exchange and/or endpoint.
*
* @param exchange The exchange
* @param endpoint The endpoint
* @return The message bus destination
*/
protected String getDestination(Exchange exchange, Endpoint endpoint) {
switch (component) {
case "cometds":
case "cometd":
return URI.create(endpoint.getEndpointUri()).getPath().substring(1);
case "rabbitmq":
return (String) exchange.getIn().getHeader("rabbitmq.EXCHANGE_NAME");
case "stomp":
String destination = stripSchemeAndOptions(endpoint);
if (destination.startsWith("queue:")) {
destination = destination.substring("queue:".length());
}
return destination;
case "mqtt":
Map<String, String> queryParameters = toQueryParameters(endpoint.getEndpointUri());
return (queryParameters.containsKey("subscribeTopicNames")
? queryParameters.get("subscribeTopicNames")
: queryParameters.get("publishTopicName"));
default:
return stripSchemeAndOptions(endpoint);
}
}
@Override
public Span.Kind getInitiatorSpanKind() {
return Kind.PRODUCER;
}
@Override
public Span.Kind getReceiverSpanKind() {
return Kind.CONSUMER;
}
/**
* This method identifies the message id for the messaging exchange.
*
* @return The message id, or null if no id exists for the exchange
*/
protected String getMessageId(Exchange exchange) {
switch (component) {
case "aws-sns":
return (String) exchange.getIn().getHeader("CamelAwsSnsMessageId");
case "aws-sqs":
return (String) exchange.getIn().getHeader("CamelAwsSqsMessageId");
case "ironmq":
return (String) exchange.getIn().getHeader("CamelIronMQMessageId");
case "jms":
return (String) exchange.getIn().getHeader("JMSMessageID");
default:
return null;
}
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Apache Camel Opentracing Component
*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apachecamel.decorators;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class RestSpanDecorator extends HttpSpanDecorator {
private static final Logger LOG = LoggerFactory.getLogger(RestSpanDecorator.class);
@Override
protected String getPath(Exchange exchange, Endpoint endpoint) {
String endpointUri = endpoint.getEndpointUri();
// Obtain the 'path' part of the URI format: rest://method:path[:uriTemplate]?[options]
String path = null;
int index = endpointUri.indexOf(':');
if (index != -1) {
index = endpointUri.indexOf(':', index + 1);
if (index != -1) {
path = endpointUri.substring(index + 1);
index = path.indexOf('?');
if (index != -1) {
path = path.substring(0, index);
}
path = path.replaceAll(":", "");
try {
path = URLDecoder.decode(path, "UTF-8");
} catch (UnsupportedEncodingException e) {
LOG.debug("Failed to decode URL path '" + path + "', ignoring exception", e);
}
}
}
return path;
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
// Includes work from:
/*
* Apache Camel Opentracing Component
*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apachecamel.decorators;
import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
class TimerSpanDecorator extends BaseSpanDecorator {
@Override
public String getOperationName(
Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) {
Object name = exchange.getProperty(Exchange.TIMER_NAME);
if (name instanceof String) {
return (String) name;
}
return super.getOperationName(exchange, endpoint, camelDirection);
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package test
import static io.opentelemetry.api.trace.Span.Kind.INTERNAL
import io.opentelemetry.instrumentation.test.AgentTestRunner
import org.apache.camel.CamelContext
import org.apache.camel.ProducerTemplate
import org.springframework.boot.SpringApplication
import org.springframework.context.ConfigurableApplicationContext
import spock.lang.Shared
class DirectCamelTest extends AgentTestRunner {
@Shared
ConfigurableApplicationContext server
def setupSpec() {
def app = new SpringApplication(DirectConfig)
server = app.run()
}
def cleanupSpec() {
if (server != null) {
server.close()
server = null
}
}
def "simple direct to a single services"() {
setup:
def camelContext = server.getBean(CamelContext)
ProducerTemplate template = camelContext.createProducerTemplate()
when:
template.sendBody("direct:input", "Example request")
then:
assertTraces(1) {
trace(0, 2) {
def parent = it
it.span(0) {
name "input"
kind INTERNAL
hasNoParent()
attributes {
"camel.uri" "direct://input"
}
}
it.span(1) {
name "receiver"
kind INTERNAL
parentSpanId parent.span(0).spanId
attributes {
"camel.uri" "direct://receiver"
}
}
}
}
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package test
import org.apache.camel.LoggingLevel
import org.apache.camel.builder.RouteBuilder
import org.springframework.boot.SpringBootConfiguration
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation.Bean
@SpringBootConfiguration
@EnableAutoConfiguration
class DirectConfig {
@Bean
RouteBuilder receiverRoute() {
return new RouteBuilder() {
@Override
void configure() throws Exception {
from("direct:receiver")
.log(LoggingLevel.INFO, "test","RECEIVER got: \${body}")
.delay(simple("2000"))
.setBody(constant("result"))
}
}
}
@Bean
RouteBuilder clientRoute() {
return new RouteBuilder() {
@Override
void configure() throws Exception {
from("direct:input")
.log(LoggingLevel.INFO, "test","SENDING request \${body}")
.to("direct:receiver")
.log(LoggingLevel.INFO, "test","RECEIVED response \${body}")
}
}
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package test
import org.apache.camel.LoggingLevel
import org.apache.camel.builder.RouteBuilder
import org.springframework.boot.SpringBootConfiguration
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation.Bean
@SpringBootConfiguration
@EnableAutoConfiguration
class MulticastConfig {
@Bean
RouteBuilder firstServiceRoute() {
return new RouteBuilder() {
@Override
void configure() throws Exception {
from("direct:first")
.log(LoggingLevel.INFO, "test","FIRST request: \${body}")
.delay(simple("1000"))
.setBody(constant("first"))
}
}
}
@Bean
RouteBuilder secondServiceRoute() {
return new RouteBuilder() {
@Override
void configure() throws Exception {
from("direct:second")
.log(LoggingLevel.INFO, "test","SECOND request: \${body}")
.delay(simple("2000"))
.setBody(constant("second"))
}
}
}
@Bean
RouteBuilder clientServiceRoute() {
return new RouteBuilder() {
@Override
void configure() throws Exception {
from("direct:input")
.log(LoggingLevel.INFO, "test","SENDING request \${body}")
.multicast()
.parallelProcessing()
.to("direct:first", "direct:second")
.end()
.log(LoggingLevel.INFO, "test","RECEIVED response \${body}")
}
}
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package test
import static io.opentelemetry.api.trace.Span.Kind.INTERNAL
import io.opentelemetry.instrumentation.test.AgentTestRunner
import org.apache.camel.CamelContext
import org.apache.camel.ProducerTemplate
import org.springframework.boot.SpringApplication
import org.springframework.context.ConfigurableApplicationContext
import spock.lang.Shared
class MulticastDirectCamelTest extends AgentTestRunner {
@Shared
ConfigurableApplicationContext server
def setupSpec() {
def app = new SpringApplication(MulticastConfig)
server = app.run()
}
def cleanupSpec() {
if (server != null) {
server.close()
server = null
}
}
def "parallel multicast to two child services"() {
setup:
def camelContext = server.getBean(CamelContext)
ProducerTemplate template = camelContext.createProducerTemplate()
when:
template.sendBody("direct:input", "Example request")
then:
assertTraces(1) {
trace(0, 3) {
def parent = it
it.span(0) {
name "input"
kind INTERNAL
hasNoParent()
attributes {
"camel.uri" "direct://input"
}
}
it.span(1) {
name "second"
kind INTERNAL
parentSpanId parent.span(0).spanId
attributes {
"camel.uri" "direct://second"
}
}
it.span(2) {
name "first"
kind INTERNAL
parentSpanId parent.span(0).spanId
attributes {
"camel.uri" "direct://first"
}
}
}
}
}
}

View File

@ -0,0 +1,117 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package test
import static io.opentelemetry.api.trace.Span.Kind.CLIENT
import static io.opentelemetry.api.trace.Span.Kind.INTERNAL
import static io.opentelemetry.api.trace.Span.Kind.SERVER
import com.google.common.collect.ImmutableMap
import io.opentelemetry.instrumentation.test.AgentTestRunner
import io.opentelemetry.instrumentation.test.utils.PortUtils
import io.opentelemetry.api.trace.attributes.SemanticAttributes
import org.apache.camel.CamelContext
import org.apache.camel.ProducerTemplate
import org.springframework.boot.SpringApplication
import org.springframework.context.ConfigurableApplicationContext
import spock.lang.Shared
class RestCamelTest extends AgentTestRunner {
@Shared
ConfigurableApplicationContext server
@Shared
int port
def setupSpec() {
withRetryOnAddressAlreadyInUse({
setupSpecUnderRetry()
})
}
def setupSpecUnderRetry() {
port = PortUtils.randomOpenPort()
def app = new SpringApplication(RestConfig)
app.setDefaultProperties(ImmutableMap.of("restServer.port", port))
server = app.run()
println getClass().name + " http server started at: http://localhost:$port/"
}
def cleanupSpec() {
if (server != null) {
server.close()
server = null
}
}
def "rest component - server and client call with jetty backend"() {
setup:
def camelContext = server.getBean(CamelContext)
ProducerTemplate template = camelContext.createProducerTemplate()
when:
// run client and server in separate threads to simulate "real" rest client/server call
new Thread(new Runnable() {
@Override
void run() {
template.sendBodyAndHeaders("direct:start", null, ImmutableMap.of("module", "firstModule", "unitId", "unitOne"))
}
}
).start()
then:
assertTraces(1) {
trace(0, 5) {
it.span(0) {
name "start"
kind INTERNAL
attributes {
"camel.uri" "direct://start"
}
}
it.span(1) {
name "GET"
kind CLIENT
attributes {
"$SemanticAttributes.HTTP_METHOD.key" "GET"
"$SemanticAttributes.HTTP_STATUS_CODE.key" 200
"camel.uri" "rest://get:api/%7Bmodule%7D/unit/%7BunitId%7D"
}
}
it.span(2) {
name "/api/{module}/unit/{unitId}"
kind SERVER
attributes {
"$SemanticAttributes.HTTP_URL.key" "http://localhost:$port/api/firstModule/unit/unitOne"
"$SemanticAttributes.HTTP_STATUS_CODE.key" 200
"$SemanticAttributes.HTTP_CLIENT_IP.key" "127.0.0.1"
"$SemanticAttributes.HTTP_USER_AGENT.key" String
"$SemanticAttributes.HTTP_FLAVOR.key" "HTTP/1.1"
"$SemanticAttributes.HTTP_METHOD.key" "GET"
"$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1"
"$SemanticAttributes.NET_PEER_PORT.key" Long
}
}
it.span(3) {
name "/api/{module}/unit/{unitId}"
kind INTERNAL
attributes {
"$SemanticAttributes.HTTP_METHOD.key" "GET"
"$SemanticAttributes.HTTP_URL.key" "http://localhost:$port/api/firstModule/unit/unitOne"
"camel.uri" String
}
}
it.span(4) {
name "moduleUnit"
kind INTERNAL
attributes {
"camel.uri" "direct://moduleUnit"
}
}
}
}
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package test
import org.apache.camel.LoggingLevel
import org.apache.camel.builder.RouteBuilder
import org.apache.camel.model.rest.RestBindingMode
import org.springframework.boot.SpringBootConfiguration
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation.Bean
@SpringBootConfiguration
@EnableAutoConfiguration
class RestConfig {
@Bean
RouteBuilder routes() {
return new RouteBuilder() {
@Override
void configure() throws Exception {
restConfiguration()
.component("jetty")
.bindingMode(RestBindingMode.auto)
.host("localhost")
.port("{{restServer.port}}")
rest("/api")
.get("/{module}/unit/{unitId}")
.to("direct:moduleUnit")
from("direct:moduleUnit")
.transform().simple("\${header.unitId} of \${header.module}")
// producer - client route
from("direct:start")
.log(LoggingLevel.INFO, "test","SENDING request")
.to("rest:get:api/{module}/unit/{unitId}")
.log(LoggingLevel.INFO, "test","RECEIVED response: '\${body}'")
}
}
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package test
import static io.opentelemetry.api.trace.Span.Kind.SERVER
import com.google.common.collect.ImmutableMap
import io.opentelemetry.instrumentation.test.AgentTestRunner
import io.opentelemetry.instrumentation.test.utils.OkHttpUtils
import io.opentelemetry.instrumentation.test.utils.PortUtils
import io.opentelemetry.api.trace.attributes.SemanticAttributes
import okhttp3.FormBody
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import org.springframework.boot.SpringApplication
import org.springframework.context.ConfigurableApplicationContext
import spock.lang.Shared
class SingleServiceCamelTest extends AgentTestRunner {
@Shared
ConfigurableApplicationContext server
@Shared
OkHttpClient client = OkHttpUtils.client()
@Shared
int port
@Shared
URI address
def setupSpec() {
withRetryOnAddressAlreadyInUse({
setupSpecUnderRetry()
})
}
def setupSpecUnderRetry() {
port = PortUtils.randomOpenPort()
address = new URI("http://localhost:$port/")
def app = new SpringApplication(SingleServiceConfig)
app.setDefaultProperties(ImmutableMap.of("camelService.port", port))
server = app.run()
println getClass().name + " http server started at: http://localhost:$port/"
}
def cleanupSpec() {
if (server != null) {
server.close()
server = null
}
}
def "single camel service span"() {
setup:
def requestUrl = address.resolve("/camelService")
def url = HttpUrl.get(requestUrl)
def request = new Request.Builder()
.url(url)
.method("POST",
new FormBody.Builder().add("", "testContent").build())
.build()
when:
client.newCall(request).execute()
then:
assertTraces(1) {
trace(0, 1) {
span(0) {
kind SERVER
name "/camelService"
attributes {
"$SemanticAttributes.HTTP_METHOD.key" "POST"
"$SemanticAttributes.HTTP_URL.key" "${address.resolve("/camelService")}"
"camel.uri" "${address.resolve("/camelService")}".replace("localhost", "0.0.0.0")
}
}
}
}
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package test
import org.apache.camel.LoggingLevel
import org.apache.camel.builder.RouteBuilder
import org.springframework.boot.SpringBootConfiguration
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation.Bean
@SpringBootConfiguration
@EnableAutoConfiguration
class SingleServiceConfig {
@Bean
RouteBuilder serviceRoute() {
return new RouteBuilder() {
@Override
void configure() throws Exception {
from("undertow:http://0.0.0.0:{{camelService.port}}/camelService")
.routeId("camelService")
.streamCaching()
.log("CamelService request: \${body}")
.delay(simple("\${random(1000, 2000)}"))
.transform(simple("CamelService-\${body}"))
.log(LoggingLevel.INFO, "test", "CamelService response: \${body}")
}
}
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package test
import org.apache.camel.builder.RouteBuilder
import org.springframework.boot.SpringBootConfiguration
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation.Bean
@SpringBootConfiguration
@EnableAutoConfiguration
class TwoServicesConfig {
@Bean
RouteBuilder serviceOneRoute() {
return new RouteBuilder() {
@Override
void configure() throws Exception {
from("undertow:http://0.0.0.0:{{service.one.port}}/serviceOne")
.routeId("serviceOne")
.streamCaching()
.removeHeaders("CamelHttp*")
.log("Service One request: \${body}")
.delay(simple("\${random(1000,2000)}"))
.transform(simple("Service-One-\${body}"))
.to("http://0.0.0.0:{{service.two.port}}/serviceTwo")
.log("Service One response: \${body}")
}
}
}
@Bean
RouteBuilder serviceTwoRoute() {
return new RouteBuilder() {
@Override
void configure() throws Exception {
from("jetty:http://0.0.0.0:{{service.two.port}}/serviceTwo?arg=value")
.routeId("serviceTwo")
.streamCaching()
.log("Service Two request: \${body}")
.delay(simple("\${random(1000, 2000)}"))
.transform(simple("Service-Two-\${body}"))
.log("Service Two response: \${body}")
}
}
}
}

View File

@ -0,0 +1,171 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package test
import static io.opentelemetry.api.trace.Span.Kind.CLIENT
import static io.opentelemetry.api.trace.Span.Kind.INTERNAL
import static io.opentelemetry.api.trace.Span.Kind.SERVER
import com.google.common.collect.ImmutableMap
import io.opentelemetry.instrumentation.test.AgentTestRunner
import io.opentelemetry.instrumentation.test.utils.PortUtils
import io.opentelemetry.api.trace.attributes.SemanticAttributes
import org.apache.camel.CamelContext
import org.apache.camel.ProducerTemplate
import org.apache.camel.builder.RouteBuilder
import org.apache.camel.impl.DefaultCamelContext
import org.springframework.boot.SpringApplication
import org.springframework.context.ConfigurableApplicationContext
import spock.lang.Shared
class TwoServicesWithDirectClientCamelTest extends AgentTestRunner {
@Shared
int portOne
@Shared
int portTwo
@Shared
ConfigurableApplicationContext server
@Shared
CamelContext clientContext
def setupSpec() {
withRetryOnAddressAlreadyInUse({
setupSpecUnderRetry()
})
}
def setupSpecUnderRetry() {
portOne = PortUtils.randomOpenPort()
portTwo = PortUtils.randomOpenPort()
def app = new SpringApplication(TwoServicesConfig)
app.setDefaultProperties(ImmutableMap.of("service.one.port", portOne, "service.two.port", portTwo))
server = app.run()
}
def createAndStartClient() {
clientContext = new DefaultCamelContext()
clientContext.addRoutes(new RouteBuilder() {
void configure() {
from("direct:input")
.log("SENT Client request")
.to("http://localhost:$portOne/serviceOne")
.log("RECEIVED Client response")
}
})
clientContext.start()
}
def cleanupSpec() {
if (server != null) {
server.close()
server = null
}
}
def "two camel service spans"() {
setup:
createAndStartClient()
ProducerTemplate template = clientContext.createProducerTemplate()
when:
template.sendBody("direct:input", "Example request")
then:
assertTraces(1) {
trace(0, 8) {
it.span(0) {
name "input"
kind INTERNAL
attributes {
"camel.uri" "direct://input"
}
}
it.span(1) {
name "POST"
kind CLIENT
attributes {
"$SemanticAttributes.HTTP_METHOD.key" "POST"
"$SemanticAttributes.HTTP_URL.key" "http://localhost:$portOne/serviceOne"
"$SemanticAttributes.HTTP_STATUS_CODE.key" 200
"camel.uri" "http://localhost:$portOne/serviceOne"
}
}
it.span(2) {
name "HTTP POST"
kind CLIENT
attributes {
"$SemanticAttributes.HTTP_METHOD.key" "POST"
"$SemanticAttributes.HTTP_URL.key" "http://localhost:$portOne/serviceOne"
"$SemanticAttributes.HTTP_STATUS_CODE.key" 200
"$SemanticAttributes.NET_PEER_NAME.key" "localhost"
"$SemanticAttributes.NET_PEER_PORT.key" portOne
"$SemanticAttributes.NET_TRANSPORT.key" "IP.TCP"
"$SemanticAttributes.HTTP_FLAVOR.key" "1.1"
}
}
it.span(3) {
name "/serviceOne"
kind SERVER
attributes {
"$SemanticAttributes.HTTP_METHOD.key" "POST"
"$SemanticAttributes.HTTP_URL.key" "http://localhost:$portOne/serviceOne"
"$SemanticAttributes.HTTP_STATUS_CODE.key" 200
"camel.uri" "http://0.0.0.0:$portOne/serviceOne"
}
}
it.span(4) {
name "POST"
kind CLIENT
attributes {
"$SemanticAttributes.HTTP_METHOD.key" "POST"
"$SemanticAttributes.HTTP_URL.key" "http://0.0.0.0:$portTwo/serviceTwo"
"$SemanticAttributes.HTTP_STATUS_CODE.key" 200
"camel.uri" "http://0.0.0.0:$portTwo/serviceTwo"
}
}
it.span(5) {
name "HTTP POST"
kind CLIENT
attributes {
"$SemanticAttributes.HTTP_METHOD.key" "POST"
"$SemanticAttributes.HTTP_URL.key" "http://0.0.0.0:$portTwo/serviceTwo"
"$SemanticAttributes.HTTP_STATUS_CODE.key" 200
"$SemanticAttributes.NET_PEER_NAME.key" "0.0.0.0"
"$SemanticAttributes.NET_PEER_PORT.key" portTwo
"$SemanticAttributes.NET_TRANSPORT.key" "IP.TCP"
"$SemanticAttributes.HTTP_FLAVOR.key" "1.1"
"$SemanticAttributes.HTTP_USER_AGENT.key" "Jakarta Commons-HttpClient/3.1"
}
}
it.span(6) {
name "/serviceTwo"
kind SERVER
attributes {
"$SemanticAttributes.HTTP_METHOD.key" "POST"
"$SemanticAttributes.HTTP_STATUS_CODE.key" 200
"$SemanticAttributes.HTTP_URL.key" "http://0.0.0.0:$portTwo/serviceTwo"
"$SemanticAttributes.NET_PEER_PORT.key" Number
"$SemanticAttributes.NET_PEER_IP.key" InetAddress.getLocalHost().getHostAddress().toString()
"$SemanticAttributes.HTTP_USER_AGENT.key" "Jakarta Commons-HttpClient/3.1"
"$SemanticAttributes.HTTP_FLAVOR.key" "HTTP/1.1"
"$SemanticAttributes.HTTP_CLIENT_IP.key" InetAddress.getLocalHost().getHostAddress().toString()
}
}
it.span(7) {
name "/serviceTwo"
kind INTERNAL
attributes {
"$SemanticAttributes.HTTP_METHOD.key" "POST"
"$SemanticAttributes.HTTP_URL.key" "http://0.0.0.0:$portTwo/serviceTwo"
"camel.uri" "jetty:http://0.0.0.0:$portTwo/serviceTwo?arg=value"
}
}
}
}
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</Pattern>
</layout>
</appender>
<root level="WARN">
<appender-ref ref="console"/>
</root>
<logger name="io.opentelemetry.javaagent.instrumentation.apachecamel" level="trace"/>
<logger name="org.apache.camel" level="debug" />
<logger name="test" level="trace"/>
</configuration>

View File

@ -0,0 +1,14 @@
apply from: "$rootDir/gradle/instrumentation.gradle"
muzzle {
pass {
group = "org.apache.httpcomponents"
module = "httpasyncclient"
versions = "[4.0,)"
assertInverse = true
}
}
dependencies {
library group: 'org.apache.httpcomponents', name: 'httpasyncclient', version: '4.0'
}

View File

@ -0,0 +1,229 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient;
import static io.opentelemetry.instrumentation.api.tracer.HttpClientTracer.DEFAULT_SPAN_NAME;
import static io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient.ApacheHttpAsyncClientTracer.tracer;
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 io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Span.Kind;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.io.IOException;
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.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.nio.ContentEncoder;
import org.apache.http.nio.IOControl;
import org.apache.http.nio.protocol.HttpAsyncRequestProducer;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpCoreContext;
public class ApacheHttpAsyncClientInstrumentation 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"))),
ApacheHttpAsyncClientInstrumentation.class.getName() + "$ClientAdvice");
}
public static class ClientAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static Span methodEnter(
@Advice.Argument(value = 0, readOnly = false) HttpAsyncRequestProducer requestProducer,
@Advice.Argument(2) HttpContext context,
@Advice.Argument(value = 3, readOnly = false) FutureCallback<?> futureCallback) {
Context parentContext = Java8BytecodeBridge.currentContext();
Span clientSpan = tracer().startSpan(DEFAULT_SPAN_NAME, Kind.CLIENT);
requestProducer = new DelegatingRequestProducer(clientSpan, requestProducer);
futureCallback =
new TraceContinuedFutureCallback<>(parentContext, clientSpan, context, futureCallback);
return clientSpan;
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.Enter Span span, @Advice.Return Object result, @Advice.Thrown Throwable throwable) {
if (throwable != null) {
tracer().endExceptionally(span, throwable);
}
}
}
public static class DelegatingRequestProducer implements HttpAsyncRequestProducer {
Span span;
HttpAsyncRequestProducer delegate;
public DelegatingRequestProducer(Span span, HttpAsyncRequestProducer delegate) {
this.span = span;
this.delegate = delegate;
}
@Override
public HttpHost getTarget() {
return delegate.getTarget();
}
@Override
public HttpRequest generateRequest() throws IOException, HttpException {
HttpRequest request = delegate.generateRequest();
span.updateName(tracer().spanNameForRequest(request));
tracer().onRequest(span, request);
// TODO (trask) expose inject separate from startScope, e.g. for async cases
Scope scope = tracer().startScope(span, request);
scope.close();
return request;
}
@Override
public void produceContent(ContentEncoder encoder, IOControl ioctrl) throws IOException {
delegate.produceContent(encoder, ioctrl);
}
@Override
public void requestCompleted(HttpContext context) {
delegate.requestCompleted(context);
}
@Override
public void failed(Exception ex) {
delegate.failed(ex);
}
@Override
public boolean isRepeatable() {
return delegate.isRepeatable();
}
@Override
public void resetRequest() throws IOException {
delegate.resetRequest();
}
@Override
public void close() throws IOException {
delegate.close();
}
}
public static class TraceContinuedFutureCallback<T> implements FutureCallback<T> {
private final Context parentContext;
private final Span clientSpan;
private final HttpContext context;
private final FutureCallback<T> delegate;
public TraceContinuedFutureCallback(
Context parentContext, Span clientSpan, HttpContext context, FutureCallback<T> delegate) {
this.parentContext = parentContext;
this.clientSpan = clientSpan;
this.context = context;
// Note: this can be null in real life, so we have to handle this carefully
this.delegate = delegate;
}
@Override
public void completed(T result) {
tracer().end(clientSpan, getResponse(context));
if (parentContext == null) {
completeDelegate(result);
} else {
try (Scope scope = parentContext.makeCurrent()) {
completeDelegate(result);
}
}
}
@Override
public void failed(Exception ex) {
// end span before calling delegate
tracer().endExceptionally(clientSpan, getResponse(context), ex);
if (parentContext == null) {
failDelegate(ex);
} else {
try (Scope scope = parentContext.makeCurrent()) {
failDelegate(ex);
}
}
}
@Override
public void cancelled() {
// end span before calling delegate
tracer().end(clientSpan, getResponse(context));
if (parentContext == null) {
cancelDelegate();
} else {
try (Scope scope = parentContext.makeCurrent()) {
cancelDelegate();
}
}
}
private void completeDelegate(T result) {
if (delegate != null) {
delegate.completed(result);
}
}
private void failDelegate(Exception ex) {
if (delegate != null) {
delegate.failed(ex);
}
}
private void cancelDelegate() {
if (delegate != null) {
delegate.cancelled();
}
}
private static HttpResponse getResponse(HttpContext context) {
return (HttpResponse) context.getAttribute(HttpCoreContext.HTTP_RESPONSE);
}
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient;
import static java.util.Arrays.asList;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.List;
@AutoService(InstrumentationModule.class)
public class ApacheHttpAsyncClientInstrumentationModule extends InstrumentationModule {
public ApacheHttpAsyncClientInstrumentationModule() {
super("apache-httpasyncclient", "apache-httpasyncclient-4.0");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return asList(
new ApacheHttpAsyncClientInstrumentation(), new ApacheHttpClientRedirectInstrumentation());
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient;
import static io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient.HttpHeadersInjectAdapter.SETTER;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.propagation.TextMapPropagator.Setter;
import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.http.Header;
import org.apache.http.HttpMessage;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.RequestLine;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpUriRequest;
import org.checkerframework.checker.nullness.qual.Nullable;
public class ApacheHttpAsyncClientTracer
extends HttpClientTracer<HttpRequest, HttpRequest, HttpResponse> {
private static final ApacheHttpAsyncClientTracer TRACER = new ApacheHttpAsyncClientTracer();
public static ApacheHttpAsyncClientTracer tracer() {
return TRACER;
}
@Override
protected String method(HttpRequest request) {
if (request instanceof HttpUriRequest) {
return ((HttpUriRequest) request).getMethod();
} else {
RequestLine requestLine = request.getRequestLine();
return requestLine == null ? null : requestLine.getMethod();
}
}
@Override
protected @Nullable String flavor(HttpRequest httpRequest) {
return httpRequest.getProtocolVersion().toString();
}
@Override
protected URI url(HttpRequest request) throws URISyntaxException {
/*
* Note: this is essentially an optimization: HttpUriRequest allows quicker access to required information.
* The downside is that we need to load HttpUriRequest which essentially means we depend on httpasyncclient
* library depending on httpclient library. Currently this seems to be the case.
*/
if (request instanceof HttpUriRequest) {
return ((HttpUriRequest) request).getURI();
} else {
RequestLine requestLine = request.getRequestLine();
return requestLine == null ? null : new URI(requestLine.getUri());
}
}
@Override
protected Integer status(HttpResponse response) {
StatusLine statusLine = response.getStatusLine();
return statusLine != null ? statusLine.getStatusCode() : null;
}
@Override
protected String requestHeader(HttpRequest request, String name) {
return header(request, name);
}
@Override
protected String responseHeader(HttpResponse response, String name) {
return header(response, name);
}
@Override
protected Setter<HttpRequest> getSetter() {
return SETTER;
}
private static String header(HttpMessage message, String name) {
Header header = message.getFirstHeader(name);
return header != null ? header.getValue() : null;
}
@Override
protected String getInstrumentationName() {
return "io.opentelemetry.javaagent.apache-httpasyncclient";
}
@Override
public String spanNameForRequest(HttpRequest httpRequest) {
return super.spanNameForRequest(httpRequest);
}
/** This method is overridden to allow other classes in this package to call it. */
@Override
public Span onRequest(Span span, HttpRequest httpRequest) {
return super.onRequest(span, httpRequest);
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.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 io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.matcher.ElementMatcher;
import org.apache.http.Header;
import org.apache.http.HttpRequest;
/**
* Early versions don't copy headers over on redirect. This instrumentation copies our headers over
* manually. Inspired by
* https://github.com/elastic/apm-agent-java/blob/master/apm-agent-plugins/apm-apache-httpclient-plugin/src/main/java/co/elastic/apm/agent/httpclient/ApacheHttpAsyncClientRedirectInstrumentation.java
*/
public class ApacheHttpClientRedirectInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("org.apache.http.client.RedirectStrategy");
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return implementsInterface(named("org.apache.http.client.RedirectStrategy"));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
isMethod()
.and(named("getRedirect"))
.and(takesArgument(0, named("org.apache.http.HttpRequest"))),
ApacheHttpClientRedirectInstrumentation.class.getName() + "$ClientRedirectAdvice");
}
public static class ClientRedirectAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
private static void onAfterExecute(
@Advice.Argument(0) HttpRequest original,
@Advice.Return(typing = Assigner.Typing.DYNAMIC) HttpRequest redirect) {
if (redirect == null) {
return;
}
// TODO this only handles W3C headers
// Apache HttpClient 4.0.1+ copies headers from original to redirect only
// if redirect headers are empty. Because we add headers
// "traceparent" and "tracestate" to redirect: it means redirect headers never
// will be empty. So in case if not-instrumented redirect had no headers,
// we just copy all not set headers from original to redirect (doing same
// thing as apache httpclient does).
if (!redirect.headerIterator().hasNext()) {
// redirect didn't have other headers besides tracing, so we need to do copy
// (same work as Apache HttpClient 4.0.1+ does w/o instrumentation)
redirect.setHeaders(original.getAllHeaders());
} else {
for (Header header : original.getAllHeaders()) {
String name = header.getName().toLowerCase();
if (name.equals("traceparent") || name.equals("tracestate")) {
if (!redirect.containsHeader(header.getName())) {
redirect.setHeader(header.getName(), header.getValue());
}
}
}
}
}
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient;
import io.opentelemetry.context.propagation.TextMapPropagator;
import org.apache.http.HttpRequest;
public class HttpHeadersInjectAdapter implements TextMapPropagator.Setter<HttpRequest> {
public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter();
@Override
public void set(HttpRequest carrier, String key, String value) {
carrier.setHeader(key, value);
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import io.opentelemetry.instrumentation.test.base.HttpClientTest
import java.util.concurrent.CompletableFuture
import org.apache.http.HttpResponse
import org.apache.http.client.config.RequestConfig
import org.apache.http.concurrent.FutureCallback
import org.apache.http.impl.nio.client.HttpAsyncClients
import org.apache.http.message.BasicHeader
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Timeout
@Timeout(5)
class ApacheHttpAsyncClientCallbackTest extends HttpClientTest {
@Shared
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(CONNECT_TIMEOUT_MS)
.build()
@AutoCleanup
@Shared
def client = HttpAsyncClients.custom().setDefaultRequestConfig(requestConfig).build()
def setupSpec() {
client.start()
}
@Override
int doRequest(String method, URI uri, Map<String, String> headers, Closure callback) {
def request = new HttpUriRequest(method, uri)
headers.entrySet().each {
request.addHeader(new BasicHeader(it.key, it.value))
}
def responseFuture = new CompletableFuture<>()
client.execute(request, new FutureCallback<HttpResponse>() {
@Override
void completed(HttpResponse result) {
callback?.call()
responseFuture.complete(result.statusLine.statusCode)
}
@Override
void failed(Exception ex) {
responseFuture.completeExceptionally(ex)
}
@Override
void cancelled() {
responseFuture.cancel(true)
}
})
return responseFuture.get()
}
@Override
Integer statusOnRedirectError() {
return 302
}
@Override
boolean testRemoteConnection() {
false // otherwise SocketTimeoutException for https requests
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import io.opentelemetry.instrumentation.test.base.HttpClientTest
import java.util.concurrent.Future
import org.apache.http.client.config.RequestConfig
import org.apache.http.impl.nio.client.HttpAsyncClients
import org.apache.http.message.BasicHeader
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Timeout
@Timeout(5)
class ApacheHttpAsyncClientNullCallbackTest extends HttpClientTest {
@Shared
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(CONNECT_TIMEOUT_MS)
.build()
@AutoCleanup
@Shared
def client = HttpAsyncClients.custom().setDefaultRequestConfig(requestConfig).build()
def setupSpec() {
client.start()
}
@Override
int doRequest(String method, URI uri, Map<String, String> headers, Closure callback) {
def request = new HttpUriRequest(method, uri)
headers.entrySet().each {
request.addHeader(new BasicHeader(it.key, it.value))
}
// The point here is to test case when callback is null - fire-and-forget style
// So to make sure request is done we start request, wait for future to finish
// and then call callback if present.
Future future = client.execute(request, null)
future.get()
if (callback != null) {
callback()
}
return future.get().statusLine.statusCode
}
@Override
Integer statusOnRedirectError() {
return 302
}
@Override
boolean testRemoteConnection() {
false // otherwise SocketTimeoutException for https requests
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import io.opentelemetry.instrumentation.test.base.HttpClientTest
import java.util.concurrent.CountDownLatch
import org.apache.http.HttpResponse
import org.apache.http.client.config.RequestConfig
import org.apache.http.concurrent.FutureCallback
import org.apache.http.impl.nio.client.HttpAsyncClients
import org.apache.http.message.BasicHeader
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Timeout
@Timeout(5)
class ApacheHttpAsyncClientTest extends HttpClientTest {
@Shared
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(CONNECT_TIMEOUT_MS)
.build()
@AutoCleanup
@Shared
def client = HttpAsyncClients.custom().setDefaultRequestConfig(requestConfig).build()
def setupSpec() {
client.start()
}
@Override
int doRequest(String method, URI uri, Map<String, String> headers, Closure callback) {
def request = new HttpUriRequest(method, uri)
headers.entrySet().each {
request.addHeader(new BasicHeader(it.key, it.value))
}
def latch = new CountDownLatch(callback == null ? 0 : 1)
def handler = callback == null ? null : new FutureCallback<HttpResponse>() {
@Override
void completed(HttpResponse result) {
callback()
latch.countDown()
}
@Override
void failed(Exception ex) {
latch.countDown()
}
@Override
void cancelled() {
latch.countDown()
}
}
def future = client.execute(request, handler)
def response = future.get()
response.entity?.content?.close() // Make sure the connection is closed.
latch.await()
response.statusLine.statusCode
}
@Override
Integer statusOnRedirectError() {
return 302
}
@Override
boolean testRemoteConnection() {
false // otherwise SocketTimeoutException for https requests
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import org.apache.http.client.methods.HttpRequestBase
class HttpUriRequest extends HttpRequestBase {
private final String methodName
HttpUriRequest(final String methodName, final URI uri) {
this.methodName = methodName
setURI(uri)
}
@Override
String getMethod() {
return methodName
}
}

View File

@ -0,0 +1,17 @@
apply from: "$rootDir/gradle/instrumentation.gradle"
muzzle {
pass {
group = "commons-httpclient"
module = "commons-httpclient"
versions = "[2.0,4.0)"
skipVersions += ["3.1-jenkins-1", "20020423"] // odd versions
assertInverse = true
}
}
dependencies {
library group: 'commons-httpclient', name: 'commons-httpclient', version: '2.0'
latestDepTestLibrary group: 'commons-httpclient', name: 'commons-httpclient', version: '3.+'
}

View File

@ -0,0 +1,101 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v2_0;
import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v2_0.CommonsHttpClientTracer.tracer;
import static io.opentelemetry.javaagent.tooling.ClassLoaderMatcher.hasClassesNamed;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.extendsClass;
import static java.util.Collections.singletonList;
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.Scope;
import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap.Depth;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
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.commons.httpclient.HttpMethod;
@AutoService(InstrumentationModule.class)
public class ApacheHttpClientInstrumentationModule extends InstrumentationModule {
public ApacheHttpClientInstrumentationModule() {
super("apache-httpclient", "apache-httpclient-2.0");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new HttpClientInstrumentation());
}
public static class HttpClientInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("org.apache.commons.httpclient.HttpClient");
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return extendsClass(named("org.apache.commons.httpclient.HttpClient"));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
isMethod()
.and(named("executeMethod"))
.and(takesArguments(3))
.and(takesArgument(1, named("org.apache.commons.httpclient.HttpMethod"))),
ApacheHttpClientInstrumentationModule.class.getName() + "$ExecAdvice");
}
}
public static class ExecAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void methodEnter(
@Advice.Argument(1) HttpMethod httpMethod,
@Advice.Local("otelSpan") Span span,
@Advice.Local("otelScope") Scope scope,
@Advice.Local("otelCallDepth") Depth callDepth) {
callDepth = tracer().getCallDepth();
if (callDepth.getAndIncrement() == 0) {
span = tracer().startSpan(httpMethod);
if (span.getSpanContext().isValid()) {
scope = tracer().startScope(span, httpMethod);
}
}
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.Argument(1) HttpMethod httpMethod,
@Advice.Thrown Throwable throwable,
@Advice.Local("otelSpan") Span span,
@Advice.Local("otelScope") Scope scope,
@Advice.Local("otelCallDepth") Depth callDepth) {
if (callDepth.decrementAndGet() == 0 && scope != null) {
scope.close();
if (throwable == null) {
tracer().end(span, httpMethod);
} else {
tracer().endExceptionally(span, httpMethod, throwable);
}
}
}
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v2_0;
import io.opentelemetry.context.propagation.TextMapPropagator.Setter;
import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer;
import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap;
import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap.Depth;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.StatusLine;
import org.apache.commons.httpclient.URIException;
public class CommonsHttpClientTracer extends HttpClientTracer<HttpMethod, HttpMethod, HttpMethod> {
private static final CommonsHttpClientTracer TRACER = new CommonsHttpClientTracer();
public static CommonsHttpClientTracer tracer() {
return TRACER;
}
@Override
protected String getInstrumentationName() {
return "io.opentelemetry.javaagent.apache-httpclient";
}
public Depth getCallDepth() {
return CallDepthThreadLocalMap.getCallDepth(HttpClient.class);
}
@Override
protected String method(HttpMethod httpMethod) {
return httpMethod.getName();
}
@Override
protected URI url(HttpMethod httpMethod) throws URISyntaxException {
try {
// org.apache.commons.httpclient.URI -> java.net.URI
return new URI(httpMethod.getURI().toString());
} catch (URIException e) {
throw new URISyntaxException("", e.getMessage());
}
}
@Override
protected Integer status(HttpMethod httpMethod) {
StatusLine statusLine = httpMethod.getStatusLine();
return statusLine == null ? null : statusLine.getStatusCode();
}
@Override
protected String requestHeader(HttpMethod httpMethod, String name) {
Header header = httpMethod.getRequestHeader(name);
return header != null ? header.getValue() : null;
}
@Override
protected String responseHeader(HttpMethod httpMethod, String name) {
Header header = httpMethod.getResponseHeader(name);
return header != null ? header.getValue() : null;
}
@Override
protected Setter<HttpMethod> getSetter() {
return HttpHeadersInjectAdapter.SETTER;
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v2_0;
import io.opentelemetry.context.propagation.TextMapPropagator;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpMethod;
public class HttpHeadersInjectAdapter implements TextMapPropagator.Setter<HttpMethod> {
public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter();
@Override
public void set(HttpMethod carrier, String key, String value) {
carrier.setRequestHeader(new Header(key, value));
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import io.opentelemetry.instrumentation.test.base.HttpClientTest
import org.apache.commons.httpclient.HttpClient
import org.apache.commons.httpclient.HttpMethod
import org.apache.commons.httpclient.methods.DeleteMethod
import org.apache.commons.httpclient.methods.GetMethod
import org.apache.commons.httpclient.methods.HeadMethod
import org.apache.commons.httpclient.methods.OptionsMethod
import org.apache.commons.httpclient.methods.PostMethod
import org.apache.commons.httpclient.methods.PutMethod
import org.apache.commons.httpclient.methods.TraceMethod
import spock.lang.Shared
import spock.lang.Timeout
@Timeout(5)
class CommonsHttpClientTest extends HttpClientTest {
@Shared
HttpClient client = new HttpClient()
def setupSpec() {
client.setConnectionTimeout(CONNECT_TIMEOUT_MS)
}
@Override
int doRequest(String method, URI uri, Map<String, String> headers, Closure callback) {
HttpMethod httpMethod
switch (method) {
case "GET":
httpMethod = new GetMethod(uri.toString())
break
case "PUT":
httpMethod = new PutMethod(uri.toString())
break
case "POST":
httpMethod = new PostMethod(uri.toString())
break
case "HEAD":
httpMethod = new HeadMethod(uri.toString())
break
case "DELETE":
httpMethod = new DeleteMethod(uri.toString())
break
case "OPTIONS":
httpMethod = new OptionsMethod(uri.toString())
break
case "TRACE":
httpMethod = new TraceMethod(uri.toString())
break
default:
throw new RuntimeException("Unsupported method: " + method)
}
headers.each { httpMethod.setRequestHeader(it.key, it.value) }
try {
client.executeMethod(httpMethod)
callback?.call()
return httpMethod.getStatusCode()
} finally {
httpMethod.releaseConnection()
}
}
@Override
boolean testRedirects() {
// Generates 4 spans
false
}
}

View File

@ -0,0 +1,27 @@
apply from: "$rootDir/gradle/instrumentation.gradle"
muzzle {
fail {
group = "commons-httpclient"
module = "commons-httpclient"
versions = "[,4.0)"
skipVersions += '3.1-jenkins-1'
}
pass {
group = "org.apache.httpcomponents"
module = "httpclient"
versions = "[4.0,)"
assertInverse = true
}
pass {
// We want to support the dropwizard clients too.
group = 'io.dropwizard'
module = 'dropwizard-client'
versions = "(,)"
assertInverse = true
}
}
dependencies {
library group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.0'
}

View File

@ -0,0 +1,51 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0;
import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0.ApacheHttpClientTracer.tracer;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap;
import io.opentelemetry.javaagent.instrumentation.api.SpanWithScope;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
public class ApacheHttpClientHelper {
public static SpanWithScope doMethodEnter(HttpUriRequest request) {
Span span = tracer().startSpan(request);
Scope scope = tracer().startScope(span, request);
return new SpanWithScope(span, scope);
}
public static void doMethodExitAndResetCallDepthThread(
SpanWithScope spanWithScope, Object result, Throwable throwable) {
if (spanWithScope == null) {
return;
}
CallDepthThreadLocalMap.reset(HttpClient.class);
doMethodExit(spanWithScope, result, throwable);
}
public static void doMethodExit(SpanWithScope spanWithScope, Object result, Throwable throwable) {
try {
Span span = spanWithScope.getSpan();
if (result instanceof HttpResponse) {
tracer().onResponse(span, (HttpResponse) result);
} // else they probably provided a ResponseHandler
if (throwable != null) {
tracer().endExceptionally(span, throwable);
} else {
tracer().end(span);
}
} finally {
spanWithScope.closeScope();
}
}
}

View File

@ -0,0 +1,273 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0;
import static io.opentelemetry.javaagent.tooling.ClassLoaderMatcher.hasClassesNamed;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface;
import static java.util.Collections.singletonList;
import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap;
import io.opentelemetry.javaagent.instrumentation.api.SpanWithScope;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.HashMap;
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.implementation.bytecode.assign.Assigner;
import net.bytebuddy.matcher.ElementMatcher;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpUriRequest;
@AutoService(InstrumentationModule.class)
public class ApacheHttpClientInstrumentationModule extends InstrumentationModule {
public ApacheHttpClientInstrumentationModule() {
super("apache-httpclient", "apache-httpclient-4.0");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new HttpClientInstrumentation());
}
public static class HttpClientInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("org.apache.http.client.HttpClient");
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return implementsInterface(named("org.apache.http.client.HttpClient"));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
Map<ElementMatcher<? super MethodDescription>, String> transformers = new HashMap<>();
// There are 8 execute(...) methods. Depending on the version, they may or may not delegate
// to eachother. Thus, all methods need to be instrumented. Because of argument position and
// type, some methods can share the same advice class. The call depth tracking ensures only 1
// span is created
transformers.put(
isMethod()
.and(named("execute"))
.and(not(isAbstract()))
.and(takesArguments(1))
.and(takesArgument(0, named("org.apache.http.client.methods.HttpUriRequest"))),
ApacheHttpClientInstrumentationModule.class.getName() + "$UriRequestAdvice");
transformers.put(
isMethod()
.and(named("execute"))
.and(not(isAbstract()))
.and(takesArguments(2))
.and(takesArgument(0, named("org.apache.http.client.methods.HttpUriRequest")))
.and(takesArgument(1, named("org.apache.http.protocol.HttpContext"))),
ApacheHttpClientInstrumentationModule.class.getName() + "$UriRequestAdvice");
transformers.put(
isMethod()
.and(named("execute"))
.and(not(isAbstract()))
.and(takesArguments(2))
.and(takesArgument(0, named("org.apache.http.client.methods.HttpUriRequest")))
.and(takesArgument(1, named("org.apache.http.client.ResponseHandler"))),
ApacheHttpClientInstrumentationModule.class.getName() + "$UriRequestWithHandlerAdvice");
transformers.put(
isMethod()
.and(named("execute"))
.and(not(isAbstract()))
.and(takesArguments(3))
.and(takesArgument(0, named("org.apache.http.client.methods.HttpUriRequest")))
.and(takesArgument(1, named("org.apache.http.client.ResponseHandler")))
.and(takesArgument(2, named("org.apache.http.protocol.HttpContext"))),
ApacheHttpClientInstrumentationModule.class.getName() + "$UriRequestWithHandlerAdvice");
transformers.put(
isMethod()
.and(named("execute"))
.and(not(isAbstract()))
.and(takesArguments(2))
.and(takesArgument(0, named("org.apache.http.HttpHost")))
.and(takesArgument(1, named("org.apache.http.HttpRequest"))),
ApacheHttpClientInstrumentationModule.class.getName() + "$RequestAdvice");
transformers.put(
isMethod()
.and(named("execute"))
.and(not(isAbstract()))
.and(takesArguments(3))
.and(takesArgument(0, named("org.apache.http.HttpHost")))
.and(takesArgument(1, named("org.apache.http.HttpRequest")))
.and(takesArgument(2, named("org.apache.http.protocol.HttpContext"))),
ApacheHttpClientInstrumentationModule.class.getName() + "$RequestAdvice");
transformers.put(
isMethod()
.and(named("execute"))
.and(not(isAbstract()))
.and(takesArguments(3))
.and(takesArgument(0, named("org.apache.http.HttpHost")))
.and(takesArgument(1, named("org.apache.http.HttpRequest")))
.and(takesArgument(2, named("org.apache.http.client.ResponseHandler"))),
ApacheHttpClientInstrumentationModule.class.getName() + "$RequestWithHandlerAdvice");
transformers.put(
isMethod()
.and(named("execute"))
.and(not(isAbstract()))
.and(takesArguments(4))
.and(takesArgument(0, named("org.apache.http.HttpHost")))
.and(takesArgument(1, named("org.apache.http.HttpRequest")))
.and(takesArgument(2, named("org.apache.http.client.ResponseHandler")))
.and(takesArgument(3, named("org.apache.http.protocol.HttpContext"))),
ApacheHttpClientInstrumentationModule.class.getName() + "$RequestWithHandlerAdvice");
return transformers;
}
}
public static class UriRequestAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static SpanWithScope methodEnter(@Advice.Argument(0) HttpUriRequest request) {
int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpClient.class);
if (callDepth > 0) {
return null;
}
return ApacheHttpClientHelper.doMethodEnter(request);
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.Enter SpanWithScope spanWithScope,
@Advice.Return Object result,
@Advice.Thrown Throwable throwable) {
ApacheHttpClientHelper.doMethodExitAndResetCallDepthThread(spanWithScope, result, throwable);
}
}
public static class UriRequestWithHandlerAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static SpanWithScope methodEnter(
@Advice.Argument(0) HttpUriRequest request,
@Advice.Argument(
value = 1,
optional = true,
typing = Assigner.Typing.DYNAMIC,
readOnly = false)
Object handler) {
int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpClient.class);
if (callDepth > 0) {
return null;
}
SpanWithScope spanWithScope = ApacheHttpClientHelper.doMethodEnter(request);
// Wrap the handler so we capture the status code
if (handler instanceof ResponseHandler) {
handler =
new WrappingStatusSettingResponseHandler(
spanWithScope.getSpan(), (ResponseHandler) handler);
}
return spanWithScope;
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.Enter SpanWithScope spanWithScope,
@Advice.Return Object result,
@Advice.Thrown Throwable throwable) {
ApacheHttpClientHelper.doMethodExitAndResetCallDepthThread(spanWithScope, result, throwable);
}
}
public static class RequestAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static SpanWithScope methodEnter(
@Advice.Argument(0) HttpHost host, @Advice.Argument(1) HttpRequest request) {
int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpClient.class);
if (callDepth > 0) {
return null;
}
if (request instanceof HttpUriRequest) {
return ApacheHttpClientHelper.doMethodEnter((HttpUriRequest) request);
} else {
return ApacheHttpClientHelper.doMethodEnter(
new HostAndRequestAsHttpUriRequest(host, request));
}
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.Enter SpanWithScope spanWithScope,
@Advice.Return Object result,
@Advice.Thrown Throwable throwable) {
ApacheHttpClientHelper.doMethodExitAndResetCallDepthThread(spanWithScope, result, throwable);
}
}
public static class RequestWithHandlerAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static SpanWithScope methodEnter(
@Advice.Argument(0) HttpHost host,
@Advice.Argument(1) HttpRequest request,
@Advice.Argument(
value = 2,
optional = true,
typing = Assigner.Typing.DYNAMIC,
readOnly = false)
Object handler) {
int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpClient.class);
if (callDepth > 0) {
return null;
}
SpanWithScope spanWithScope;
if (request instanceof HttpUriRequest) {
spanWithScope = ApacheHttpClientHelper.doMethodEnter((HttpUriRequest) request);
} else {
spanWithScope =
ApacheHttpClientHelper.doMethodEnter(new HostAndRequestAsHttpUriRequest(host, request));
}
// Wrap the handler so we capture the status code
if (handler instanceof ResponseHandler) {
handler =
new WrappingStatusSettingResponseHandler(
spanWithScope.getSpan(), (ResponseHandler) handler);
}
return spanWithScope;
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.Enter SpanWithScope spanWithScope,
@Advice.Return Object result,
@Advice.Thrown Throwable throwable) {
ApacheHttpClientHelper.doMethodExitAndResetCallDepthThread(spanWithScope, result, throwable);
}
}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0;
import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0.HttpHeadersInjectAdapter.SETTER;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.propagation.TextMapPropagator.Setter;
import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer;
import java.net.URI;
import org.apache.http.Header;
import org.apache.http.HttpMessage;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.checkerframework.checker.nullness.qual.Nullable;
class ApacheHttpClientTracer
extends HttpClientTracer<HttpUriRequest, HttpUriRequest, HttpResponse> {
private static final ApacheHttpClientTracer TRACER = new ApacheHttpClientTracer();
public static ApacheHttpClientTracer tracer() {
return TRACER;
}
@Override
protected String method(HttpUriRequest httpRequest) {
return httpRequest.getMethod();
}
@Override
protected @Nullable String flavor(HttpUriRequest httpUriRequest) {
return httpUriRequest.getProtocolVersion().toString();
}
@Override
protected URI url(HttpUriRequest request) {
return request.getURI();
}
@Override
protected Integer status(HttpResponse httpResponse) {
return httpResponse.getStatusLine().getStatusCode();
}
@Override
protected String requestHeader(HttpUriRequest request, String name) {
return header(request, name);
}
@Override
protected String responseHeader(HttpResponse response, String name) {
return header(response, name);
}
@Override
protected Setter<HttpUriRequest> getSetter() {
return SETTER;
}
private static String header(HttpMessage message, String name) {
Header header = message.getFirstHeader(name);
return header != null ? header.getValue() : null;
}
@Override
protected String getInstrumentationName() {
return "io.opentelemetry.javaagent.apache-httpclient";
}
/** This method is overridden to allow other classes in this package to call it. */
@Override
protected Span onResponse(Span span, HttpResponse httpResponse) {
return super.onResponse(span, httpResponse);
}
}

View File

@ -0,0 +1,81 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.ProtocolVersion;
import org.apache.http.RequestLine;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.message.AbstractHttpMessage;
/** Wraps HttpHost and HttpRequest into a HttpUriRequest for tracers and injectors. */
public class HostAndRequestAsHttpUriRequest extends AbstractHttpMessage implements HttpUriRequest {
private final String method;
private final RequestLine requestLine;
private final ProtocolVersion protocolVersion;
private final java.net.URI uri;
private final HttpRequest actualRequest;
public HostAndRequestAsHttpUriRequest(HttpHost httpHost, HttpRequest httpRequest) {
method = httpRequest.getRequestLine().getMethod();
requestLine = httpRequest.getRequestLine();
protocolVersion = requestLine.getProtocolVersion();
URI calculatedUri;
try {
calculatedUri = new URI(httpHost.toURI() + httpRequest.getRequestLine().getUri());
} catch (URISyntaxException e) {
calculatedUri = null;
}
uri = calculatedUri;
actualRequest = httpRequest;
}
@Override
public void abort() throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
@Override
public boolean isAborted() {
return false;
}
@Override
public void addHeader(String name, String value) {
actualRequest.addHeader(name, value);
}
@Override
public String getMethod() {
return method;
}
@Override
public RequestLine getRequestLine() {
return requestLine;
}
@Override
public ProtocolVersion getProtocolVersion() {
return protocolVersion;
}
@Override
public java.net.URI getURI() {
return uri;
}
public HttpRequest getActualRequest() {
return actualRequest;
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0;
import io.opentelemetry.context.propagation.TextMapPropagator;
import org.apache.http.client.methods.HttpUriRequest;
class HttpHeadersInjectAdapter implements TextMapPropagator.Setter<HttpUriRequest> {
public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter();
@Override
public void set(HttpUriRequest carrier, String key, String value) {
carrier.addHeader(key, value);
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0;
import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0.ApacheHttpClientTracer.tracer;
import io.opentelemetry.api.trace.Span;
import java.io.IOException;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
public class WrappingStatusSettingResponseHandler implements ResponseHandler {
final Span span;
final ResponseHandler handler;
public WrappingStatusSettingResponseHandler(Span span, ResponseHandler handler) {
this.span = span;
this.handler = handler;
}
@Override
public Object handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
if (null != span) {
tracer().onResponse(span, response);
}
return handler.handleResponse(response);
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import io.opentelemetry.instrumentation.test.base.HttpClientTest
import org.apache.http.HttpResponse
import org.apache.http.client.ResponseHandler
import org.apache.http.impl.client.DefaultHttpClient
import org.apache.http.message.BasicHeader
import org.apache.http.params.HttpConnectionParams
import org.apache.http.params.HttpParams
import spock.lang.Shared
import spock.lang.Timeout
@Timeout(5)
class ApacheHttpClientResponseHandlerTest extends HttpClientTest {
@Shared
def client = new DefaultHttpClient()
@Shared
def handler = new ResponseHandler<Integer>() {
@Override
Integer handleResponse(HttpResponse response) {
return response.statusLine.statusCode
}
}
def setupSpec() {
HttpParams httpParams = client.getParams()
HttpConnectionParams.setConnectionTimeout(httpParams, CONNECT_TIMEOUT_MS)
}
@Override
int doRequest(String method, URI uri, Map<String, String> headers, Closure callback) {
def request = new HttpUriRequest(method, uri)
headers.entrySet().each {
request.addHeader(new BasicHeader(it.key, it.value))
}
def status = client.execute(request, handler)
// handler execution is included within the client span, so we can't call the callback there.
callback?.call()
return status
}
}

View File

@ -0,0 +1,187 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import io.opentelemetry.instrumentation.test.base.HttpClientTest
import org.apache.http.HttpHost
import org.apache.http.HttpRequest
import org.apache.http.HttpResponse
import org.apache.http.impl.client.DefaultHttpClient
import org.apache.http.message.BasicHeader
import org.apache.http.message.BasicHttpRequest
import org.apache.http.params.HttpConnectionParams
import org.apache.http.params.HttpParams
import org.apache.http.protocol.BasicHttpContext
import spock.lang.Shared
import spock.lang.Timeout
abstract class ApacheHttpClientTest<T extends HttpRequest> extends HttpClientTest {
@Shared
def client = new DefaultHttpClient()
def setupSpec() {
HttpParams httpParams = client.getParams()
HttpConnectionParams.setConnectionTimeout(httpParams, CONNECT_TIMEOUT_MS)
}
@Override
int doRequest(String method, URI uri, Map<String, String> headers, Closure callback) {
def request = createRequest(method, uri)
headers.entrySet().each {
request.addHeader(new BasicHeader(it.key, it.value))
}
def response = executeRequest(request, uri)
callback?.call()
response.entity?.content?.close() // Make sure the connection is closed.
return response.statusLine.statusCode
}
abstract T createRequest(String method, URI uri)
abstract HttpResponse executeRequest(T request, URI uri)
static String fullPathFromURI(URI uri) {
StringBuilder builder = new StringBuilder()
if (uri.getPath() != null) {
builder.append(uri.getPath())
}
if (uri.getQuery() != null) {
builder.append('?')
builder.append(uri.getQuery())
}
if (uri.getFragment() != null) {
builder.append('#')
builder.append(uri.getFragment())
}
return builder.toString()
}
}
@Timeout(5)
class ApacheClientHostRequest extends ApacheHttpClientTest<BasicHttpRequest> {
@Override
BasicHttpRequest createRequest(String method, URI uri) {
return new BasicHttpRequest(method, fullPathFromURI(uri))
}
@Override
HttpResponse executeRequest(BasicHttpRequest request, URI uri) {
return client.execute(new HttpHost(uri.getHost(), uri.getPort()), request)
}
@Override
boolean testRemoteConnection() {
return false
}
}
@Timeout(5)
class ApacheClientHostRequestContext extends ApacheHttpClientTest<BasicHttpRequest> {
@Override
BasicHttpRequest createRequest(String method, URI uri) {
return new BasicHttpRequest(method, fullPathFromURI(uri))
}
@Override
HttpResponse executeRequest(BasicHttpRequest request, URI uri) {
return client.execute(new HttpHost(uri.getHost(), uri.getPort()), request, new BasicHttpContext())
}
@Override
boolean testRemoteConnection() {
return false
}
}
@Timeout(5)
class ApacheClientHostRequestResponseHandler extends ApacheHttpClientTest<BasicHttpRequest> {
@Override
BasicHttpRequest createRequest(String method, URI uri) {
return new BasicHttpRequest(method, fullPathFromURI(uri))
}
@Override
HttpResponse executeRequest(BasicHttpRequest request, URI uri) {
return client.execute(new HttpHost(uri.getHost(), uri.getPort()), request, { response -> response })
}
@Override
boolean testRemoteConnection() {
return false
}
}
@Timeout(5)
class ApacheClientHostRequestResponseHandlerContext extends ApacheHttpClientTest<BasicHttpRequest> {
@Override
BasicHttpRequest createRequest(String method, URI uri) {
return new BasicHttpRequest(method, fullPathFromURI(uri))
}
@Override
HttpResponse executeRequest(BasicHttpRequest request, URI uri) {
return client.execute(new HttpHost(uri.getHost(), uri.getPort()), request, { response -> response }, new BasicHttpContext())
}
@Override
boolean testRemoteConnection() {
return false
}
}
@Timeout(5)
class ApacheClientUriRequest extends ApacheHttpClientTest<HttpUriRequest> {
@Override
HttpUriRequest createRequest(String method, URI uri) {
return new HttpUriRequest(method, uri)
}
@Override
HttpResponse executeRequest(HttpUriRequest request, URI uri) {
return client.execute(request)
}
}
@Timeout(5)
class ApacheClientUriRequestContext extends ApacheHttpClientTest<HttpUriRequest> {
@Override
HttpUriRequest createRequest(String method, URI uri) {
return new HttpUriRequest(method, uri)
}
@Override
HttpResponse executeRequest(HttpUriRequest request, URI uri) {
return client.execute(request, new BasicHttpContext())
}
}
@Timeout(5)
class ApacheClientUriRequestResponseHandler extends ApacheHttpClientTest<HttpUriRequest> {
@Override
HttpUriRequest createRequest(String method, URI uri) {
return new HttpUriRequest(method, uri)
}
@Override
HttpResponse executeRequest(HttpUriRequest request, URI uri) {
return client.execute(request, { response -> response })
}
}
@Timeout(5)
class ApacheClientUriRequestResponseHandlerContext extends ApacheHttpClientTest<HttpUriRequest> {
@Override
HttpUriRequest createRequest(String method, URI uri) {
return new HttpUriRequest(method, uri)
}
@Override
HttpResponse executeRequest(HttpUriRequest request, URI uri) {
return client.execute(request, { response -> response })
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import org.apache.http.client.methods.HttpRequestBase
class HttpUriRequest extends HttpRequestBase {
private final String methodName
HttpUriRequest(final String methodName, final URI uri) {
this.methodName = methodName
setURI(uri)
}
@Override
String getMethod() {
return methodName
}
}

View File

@ -0,0 +1,14 @@
apply from: "$rootDir/gradle/instrumentation.gradle"
muzzle {
pass {
group = "com.ning"
module = "async-http-client"
versions = "[1.9.0,)"
assertInverse = true
}
}
dependencies {
library group: 'com.ning', name: 'async-http-client', version: '1.9.0'
}

View File

@ -0,0 +1,19 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.asynchttpclient;
import com.ning.http.client.Request;
import io.opentelemetry.context.propagation.TextMapPropagator;
public class AsyncHttpClientInjectAdapter implements TextMapPropagator.Setter<Request> {
public static final AsyncHttpClientInjectAdapter SETTER = new AsyncHttpClientInjectAdapter();
@Override
public void set(Request carrier, String key, String value) {
carrier.getHeaders().replaceWith(key, value);
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.asynchttpclient;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonMap;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.instrumentation.api.Pair;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.List;
import java.util.Map;
@AutoService(io.opentelemetry.javaagent.tooling.InstrumentationModule.class)
public class AsyncHttpClientInstrumentationModule
extends io.opentelemetry.javaagent.tooling.InstrumentationModule {
public AsyncHttpClientInstrumentationModule() {
super("async-http-client", "async-http-client-1.9");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return asList(new RequestInstrumentation(), new ResponseInstrumentation());
}
@Override
public Map<String, String> contextStore() {
return singletonMap("com.ning.http.client.AsyncHandler", Pair.class.getName());
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.asynchttpclient;
import com.ning.http.client.Request;
import com.ning.http.client.Response;
import io.opentelemetry.context.propagation.TextMapPropagator.Setter;
import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer;
import java.net.URI;
import java.net.URISyntaxException;
public class AsyncHttpClientTracer extends HttpClientTracer<Request, Request, Response> {
private static final AsyncHttpClientTracer TRACER = new AsyncHttpClientTracer();
public static AsyncHttpClientTracer tracer() {
return TRACER;
}
@Override
protected String method(Request request) {
return request.getMethod();
}
@Override
protected URI url(Request request) throws URISyntaxException {
return request.getUri().toJavaNetURI();
}
@Override
protected Integer status(Response response) {
return response.getStatusCode();
}
@Override
protected String requestHeader(Request request, String name) {
return request.getHeaders().getFirstValue(name);
}
@Override
protected String responseHeader(Response response, String name) {
return response.getHeaders().getFirstValue(name);
}
@Override
protected Setter<Request> getSetter() {
return AsyncHttpClientInjectAdapter.SETTER;
}
@Override
protected String getInstrumentationName() {
return "io.opentelemetry.javaagent.async-http-client";
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.asynchttpclient;
import com.ning.http.client.AsyncHandler;
import com.ning.http.client.Request;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
import io.opentelemetry.javaagent.instrumentation.api.Pair;
import net.bytebuddy.asm.Advice;
public class RequestAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static Scope onEnter(
@Advice.Argument(0) Request request, @Advice.Argument(1) AsyncHandler<?> handler) {
Context parentContext = Java8BytecodeBridge.currentContext();
Span span = AsyncHttpClientTracer.tracer().startSpan(request);
InstrumentationContext.get(AsyncHandler.class, Pair.class)
.put(handler, Pair.of(parentContext, span));
return AsyncHttpClientTracer.tracer().startScope(span, request);
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(@Advice.Enter Scope scope) {
// span closed in ClientResponseAdvice
scope.close();
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.asynchttpclient;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.Map;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
public class RequestInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return named("com.ning.http.client.AsyncHttpClient");
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("executeRequest")
.and(takesArgument(0, named("com.ning.http.client.Request")))
.and(takesArgument(1, named("com.ning.http.client.AsyncHandler")))
.and(isPublic()),
RequestAdvice.class.getName());
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.asynchttpclient;
import com.ning.http.client.AsyncCompletionHandler;
import com.ning.http.client.AsyncHandler;
import com.ning.http.client.Response;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.instrumentation.api.ContextStore;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import io.opentelemetry.javaagent.instrumentation.api.Pair;
import net.bytebuddy.asm.Advice;
public class ResponseAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static Scope onEnter(
@Advice.This AsyncCompletionHandler<?> handler, @Advice.Argument(0) Response response) {
// TODO I think all this should happen on exit, not on enter.
// After response was handled by user provided handler.
ContextStore<AsyncHandler, Pair> contextStore =
InstrumentationContext.get(AsyncHandler.class, Pair.class);
Pair<Context, Span> spanWithParent = contextStore.get(handler);
if (null != spanWithParent) {
contextStore.put(handler, null);
}
if (spanWithParent.hasRight()) {
AsyncHttpClientTracer.tracer().end(spanWithParent.getRight(), response);
}
return spanWithParent.hasLeft() ? spanWithParent.getLeft().makeCurrent() : null;
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(@Advice.Enter Scope scope) {
if (null != scope) {
scope.close();
}
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.asynchttpclient;
import static io.opentelemetry.javaagent.tooling.ClassLoaderMatcher.hasClassesNamed;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.hasSuperClass;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.Map;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
public class ResponseInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("com.ning.http.client.AsyncCompletionHandler");
}
@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return hasSuperClass(named("com.ning.http.client.AsyncCompletionHandler"));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("onCompleted")
.and(takesArgument(0, named("com.ning.http.client.Response")))
.and(isPublic()),
ResponseAdvice.class.getName());
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import com.ning.http.client.AsyncCompletionHandler
import com.ning.http.client.AsyncHttpClient
import com.ning.http.client.Request
import com.ning.http.client.RequestBuilder
import com.ning.http.client.Response
import com.ning.http.client.uri.Uri
import io.opentelemetry.instrumentation.test.base.HttpClientTest
import spock.lang.AutoCleanup
import spock.lang.Shared
class AsyncHttpClientTest extends HttpClientTest {
@AutoCleanup
@Shared
def client = new AsyncHttpClient()
@Override
int doRequest(String method, URI uri, Map<String, String> headers, Closure callback) {
RequestBuilder requestBuilder = new RequestBuilder(method)
.setUri(Uri.create(uri.toString()))
headers.entrySet().each {
requestBuilder.addHeader(it.key, it.value)
}
Request request = requestBuilder.build()
def handler = new AsyncCompletionHandler() {
@Override
Object onCompleted(Response response) throws Exception {
if (callback != null) {
callback()
}
return response
}
}
def response = client.executeRequest(request, handler).get()
response.statusCode
}
@Override
boolean testRedirects() {
false
}
@Override
boolean testConnectionFailure() {
false
}
@Override
boolean testRemoteConnection() {
return false
}
}

View File

@ -0,0 +1,72 @@
apply from: "$rootDir/gradle/instrumentation.gradle"
apply plugin: 'org.unbroken-dome.test-sets'
// compiling against 1.11.0, but instrumentation should work against 1.10.33 with varying effects,
// depending on the version's implementation. (i.e. DeleteOptionGroup may have less handlerCounts than
// expected in 1.11.84. Testing against 1.11.0 instead of 1.10.33 because the RequestHandler class
// used in testing is abstract in 1.10.33
// keeping base test version on 1.11.0 because RequestHandler2 is abstract in 1.10.33,
// therefore keeping base version as 1.11.0 even though the instrumentation probably
// is able to support up to 1.10.33
muzzle {
pass {
group = "com.amazonaws"
module = "aws-java-sdk-core"
versions = "[1.10.33,)"
assertInverse = true
}
}
testSets {
// Features used in test_1_11_106 (builder) is available since 1.11.84, but
// using 1.11.106 because of previous concerns with byte code differences
// in 1.11.106, also, the DeleteOptionGroup request generates more spans
// in 1.11.106 than 1.11.84.
// We test older version in separate test set to test newer version and latest deps in the 'default'
// test dir. Otherwise we get strange warnings in Idea.
test_before_1_11_106 {
filter {
// this is needed because "test.dependsOn test_before_1_11_106", and so without this,
// running a single test in the default test set will fail
setFailOnNoMatchingTests(false)
}
}
}
configurations {
test_before_1_11_106RuntimeClasspath {
resolutionStrategy.force 'com.amazonaws:aws-java-sdk-s3:1.11.0'
resolutionStrategy.force 'com.amazonaws:aws-java-sdk-rds:1.11.0'
resolutionStrategy.force 'com.amazonaws:aws-java-sdk-ec2:1.11.0'
resolutionStrategy.force 'com.amazonaws:aws-java-sdk-kinesis:1.11.0'
resolutionStrategy.force 'com.amazonaws:aws-java-sdk-sqs:1.11.0'
resolutionStrategy.force 'com.amazonaws:aws-java-sdk-dynamodb:1.11.0'
}
}
dependencies {
library group: 'com.amazonaws', name: 'aws-java-sdk-core', version: '1.11.0'
// Include httpclient instrumentation for testing because it is a dependency for aws-sdk.
testImplementation project(':instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent')
testLibrary group: 'com.amazonaws', name: 'aws-java-sdk-s3', version: '1.11.106'
testLibrary group: 'com.amazonaws', name: 'aws-java-sdk-rds', version: '1.11.106'
testLibrary group: 'com.amazonaws', name: 'aws-java-sdk-ec2', version: '1.11.106'
testLibrary group: 'com.amazonaws', name: 'aws-java-sdk-kinesis', version: '1.11.106'
testLibrary group: 'com.amazonaws', name: 'aws-java-sdk-sqs', version: '1.11.106'
testLibrary group: 'com.amazonaws', name: 'aws-java-sdk-dynamodb', version: '1.11.106'
// needed for kinesis:
testImplementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-cbor', version: versions.jackson
test_before_1_11_106Implementation(group: 'com.amazonaws', name: 'aws-java-sdk-s3', version: '1.11.0')
test_before_1_11_106Implementation(group: 'com.amazonaws', name: 'aws-java-sdk-rds', version: '1.11.0')
test_before_1_11_106Implementation(group: 'com.amazonaws', name: 'aws-java-sdk-ec2', version: '1.11.0')
test_before_1_11_106Implementation(group: 'com.amazonaws', name: 'aws-java-sdk-kinesis', version: '1.11.0')
test_before_1_11_106Implementation(group: 'com.amazonaws', name: 'aws-java-sdk-sqs', version: '1.11.0')
test_before_1_11_106Implementation(group: 'com.amazonaws', name: 'aws-java-sdk-dynamodb', version: '1.11.0')
}
if (!testLatestDeps) {
test.dependsOn test_before_1_11_106
}

View File

@ -0,0 +1,61 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.declaresField;
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
import static net.bytebuddy.matcher.ElementMatchers.named;
import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.handlers.RequestHandler2;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
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;
/**
* This instrumentation might work with versions before 1.11.0, but this was the first version that
* is tested. It could possibly be extended earlier.
*/
public class AwsClientInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("com.amazonaws.AmazonWebServiceClient")
.and(declaresField(named("requestHandler2s")));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
isConstructor(), AwsClientInstrumentation.class.getName() + "$AwsClientAdvice");
}
public static class AwsClientAdvice {
// Since we're instrumenting the constructor, we can't add onThrowable.
@Advice.OnMethodExit(suppress = Throwable.class)
public static void addHandler(
@Advice.FieldValue("requestHandler2s") List<RequestHandler2> handlers) {
boolean hasAgentHandler = false;
for (RequestHandler2 handler : handlers) {
if (handler instanceof TracingRequestHandler) {
hasAgentHandler = true;
break;
}
}
if (!hasAgentHandler) {
handlers.add(
new TracingRequestHandler(
InstrumentationContext.get(AmazonWebServiceRequest.class, RequestMeta.class)));
}
}
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11;
import static io.opentelemetry.javaagent.instrumentation.awssdk.v1_11.AwsSdkClientTracer.tracer;
import static io.opentelemetry.javaagent.instrumentation.awssdk.v1_11.RequestMeta.SPAN_SCOPE_PAIR_CONTEXT_KEY;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import com.amazonaws.AmazonClientException;
import com.amazonaws.Request;
import com.amazonaws.handlers.RequestHandler2;
import io.opentelemetry.javaagent.instrumentation.api.SpanWithScope;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
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;
/**
* This is additional 'helper' to catch cases when HTTP request throws exception different from
* {@link AmazonClientException} (for example an error thrown by another handler). In these cases
* {@link RequestHandler2#afterError} is not called.
*/
public class AwsHttpClientInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("com.amazonaws.http.AmazonHttpClient");
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
isMethod().and(not(isAbstract())).and(named("doExecute")),
AwsHttpClientInstrumentation.class.getName() + "$HttpClientAdvice");
}
public static class HttpClientAdvice {
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.Argument(value = 0, optional = true) Request<?> request,
@Advice.Thrown Throwable throwable) {
if (throwable != null) {
SpanWithScope scope = request.getHandlerContext(SPAN_SCOPE_PAIR_CONTEXT_KEY);
if (scope != null) {
request.addHandlerContext(SPAN_SCOPE_PAIR_CONTEXT_KEY, null);
tracer().endExceptionally(scope.getSpan(), throwable);
scope.closeScope();
}
}
}
}
}

View File

@ -0,0 +1,127 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11;
import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.AmazonWebServiceResponse;
import com.amazonaws.Request;
import com.amazonaws.Response;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.propagation.TextMapPropagator.Setter;
import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer;
import java.net.URI;
import java.util.concurrent.ConcurrentHashMap;
public class AwsSdkClientTracer extends HttpClientTracer<Request<?>, Request<?>, Response<?>> {
static final String COMPONENT_NAME = "java-aws-sdk";
private static final AwsSdkClientTracer TRACER = new AwsSdkClientTracer();
public static AwsSdkClientTracer tracer() {
return TRACER;
}
private final NamesCache namesCache = new NamesCache();
public AwsSdkClientTracer() {}
@Override
public String spanNameForRequest(Request<?> request) {
if (request == null) {
return DEFAULT_SPAN_NAME;
}
String awsServiceName = request.getServiceName();
Class<?> awsOperation = request.getOriginalRequest().getClass();
return qualifiedOperation(awsServiceName, awsOperation);
}
public Span startSpan(Request<?> request, RequestMeta requestMeta) {
Span span = super.startSpan(request);
String awsServiceName = request.getServiceName();
AmazonWebServiceRequest originalRequest = request.getOriginalRequest();
Class<?> awsOperation = originalRequest.getClass();
span.setAttribute("aws.agent", COMPONENT_NAME);
span.setAttribute("aws.service", awsServiceName);
span.setAttribute("aws.operation", awsOperation.getSimpleName());
span.setAttribute("aws.endpoint", request.getEndpoint().toString());
if (requestMeta != null) {
span.setAttribute("aws.bucket.name", requestMeta.getBucketName());
span.setAttribute("aws.queue.url", requestMeta.getQueueUrl());
span.setAttribute("aws.queue.name", requestMeta.getQueueName());
span.setAttribute("aws.stream.name", requestMeta.getStreamName());
span.setAttribute("aws.table.name", requestMeta.getTableName());
}
return span;
}
@Override
public Span onResponse(Span span, Response<?> response) {
if (response != null && response.getAwsResponse() instanceof AmazonWebServiceResponse) {
AmazonWebServiceResponse awsResp = (AmazonWebServiceResponse) response.getAwsResponse();
span.setAttribute("aws.requestId", awsResp.getRequestId());
}
return super.onResponse(span, response);
}
private String qualifiedOperation(String service, Class<?> operation) {
ConcurrentHashMap<String, String> cache = namesCache.get(operation);
String qualified = cache.get(service);
if (qualified == null) {
qualified =
service.replace("Amazon", "").trim()
+ '.'
+ operation.getSimpleName().replace("Request", "");
cache.put(service, qualified);
}
return qualified;
}
@Override
protected String method(Request<?> request) {
return request.getHttpMethod().name();
}
@Override
protected URI url(Request<?> request) {
return request.getEndpoint();
}
@Override
protected Integer status(Response<?> response) {
return response.getHttpResponse().getStatusCode();
}
@Override
protected String requestHeader(Request<?> request, String name) {
return request.getHeaders().get(name);
}
@Override
protected String responseHeader(Response<?> response, String name) {
return response.getHttpResponse().getHeaders().get(name);
}
@Override
protected Setter<Request<?>> getSetter() {
return AwsSdkInjectAdapter.INSTANCE;
}
@Override
protected String getInstrumentationName() {
return "io.opentelemetry.javaagent.aws-sdk";
}
static final class NamesCache extends ClassValue<ConcurrentHashMap<String, String>> {
@Override
protected ConcurrentHashMap<String, String> computeValue(Class<?> type) {
return new ConcurrentHashMap<>();
}
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11;
import com.amazonaws.Request;
import io.opentelemetry.context.propagation.TextMapPropagator;
final class AwsSdkInjectAdapter implements TextMapPropagator.Setter<Request<?>> {
static final AwsSdkInjectAdapter INSTANCE = new AwsSdkInjectAdapter();
@Override
public void set(Request<?> request, String name, String value) {
request.addHeader(name, value);
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonMap;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.List;
import java.util.Map;
@AutoService(InstrumentationModule.class)
public class AwsSdkInstrumentationModule extends InstrumentationModule {
public AwsSdkInstrumentationModule() {
super("aws-sdk", "aws-sdk-1.11");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return asList(
new AwsClientInstrumentation(),
new AwsHttpClientInstrumentation(),
new RequestExecutorInstrumentation(),
new RequestInstrumentation());
}
@Override
public Map<String, String> contextStore() {
return singletonMap(
"com.amazonaws.AmazonWebServiceRequest",
"io.opentelemetry.javaagent.instrumentation.awssdk.v1_11.RequestMeta");
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11;
import static io.opentelemetry.javaagent.instrumentation.awssdk.v1_11.AwsSdkClientTracer.tracer;
import static io.opentelemetry.javaagent.instrumentation.awssdk.v1_11.RequestMeta.SPAN_SCOPE_PAIR_CONTEXT_KEY;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import com.amazonaws.Request;
import io.opentelemetry.javaagent.instrumentation.api.SpanWithScope;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
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;
/**
* Due to a change in the AmazonHttpClient class, this instrumentation is needed to support newer
* versions. The {@link AwsHttpClientInstrumentation} class should cover older versions.
*/
public class RequestExecutorInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("com.amazonaws.http.AmazonHttpClient$RequestExecutor");
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
isMethod().and(not(isAbstract())).and(named("doExecute")),
RequestExecutorInstrumentation.class.getName() + "$RequestExecutorAdvice");
}
public static class RequestExecutorAdvice {
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.FieldValue("request") Request<?> request, @Advice.Thrown Throwable throwable) {
if (throwable != null) {
SpanWithScope scope = request.getHandlerContext(SPAN_SCOPE_PAIR_CONTEXT_KEY);
if (scope != null) {
request.addHandlerContext(SPAN_SCOPE_PAIR_CONTEXT_KEY, null);
tracer().endExceptionally(scope.getSpan(), throwable);
scope.closeScope();
}
}
}
}
}

View File

@ -0,0 +1,133 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11;
import static io.opentelemetry.javaagent.tooling.ClassLoaderMatcher.hasClassesNamed;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.extendsClass;
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import com.amazonaws.AmazonWebServiceRequest;
import io.opentelemetry.javaagent.instrumentation.api.ContextStore;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.HashMap;
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;
public class RequestInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("com.amazonaws.AmazonWebServiceRequest");
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return nameStartsWith("com.amazonaws.services.")
.and(extendsClass(named("com.amazonaws.AmazonWebServiceRequest")));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
Map<ElementMatcher<? super MethodDescription>, String> transformers = new HashMap<>();
transformers.put(
named("setBucketName").and(takesArgument(0, String.class)),
RequestInstrumentation.class.getName() + "$BucketNameAdvice");
transformers.put(
named("setQueueUrl").and(takesArgument(0, String.class)),
RequestInstrumentation.class.getName() + "$QueueUrlAdvice");
transformers.put(
named("setQueueName").and(takesArgument(0, String.class)),
RequestInstrumentation.class.getName() + "$QueueNameAdvice");
transformers.put(
named("setStreamName").and(takesArgument(0, String.class)),
RequestInstrumentation.class.getName() + "$StreamNameAdvice");
transformers.put(
named("setTableName").and(takesArgument(0, String.class)),
RequestInstrumentation.class.getName() + "$TableNameAdvice");
return transformers;
}
public static class BucketNameAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void methodEnter(
@Advice.Argument(0) String value, @Advice.This AmazonWebServiceRequest request) {
ContextStore<AmazonWebServiceRequest, RequestMeta> contextStore =
InstrumentationContext.get(AmazonWebServiceRequest.class, RequestMeta.class);
RequestMeta requestMeta = contextStore.get(request);
if (requestMeta == null) {
requestMeta = new RequestMeta();
contextStore.put(request, requestMeta);
}
requestMeta.setBucketName(value);
}
}
public static class QueueUrlAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void methodEnter(
@Advice.Argument(0) String value, @Advice.This AmazonWebServiceRequest request) {
ContextStore<AmazonWebServiceRequest, RequestMeta> contextStore =
InstrumentationContext.get(AmazonWebServiceRequest.class, RequestMeta.class);
RequestMeta requestMeta = contextStore.get(request);
if (requestMeta == null) {
requestMeta = new RequestMeta();
contextStore.put(request, requestMeta);
}
requestMeta.setQueueUrl(value);
}
}
public static class QueueNameAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void methodEnter(
@Advice.Argument(0) String value, @Advice.This AmazonWebServiceRequest request) {
ContextStore<AmazonWebServiceRequest, RequestMeta> contextStore =
InstrumentationContext.get(AmazonWebServiceRequest.class, RequestMeta.class);
RequestMeta requestMeta = contextStore.get(request);
if (requestMeta == null) {
requestMeta = new RequestMeta();
contextStore.put(request, requestMeta);
}
requestMeta.setQueueName(value);
}
}
public static class StreamNameAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void methodEnter(
@Advice.Argument(0) String value, @Advice.This AmazonWebServiceRequest request) {
ContextStore<AmazonWebServiceRequest, RequestMeta> contextStore =
InstrumentationContext.get(AmazonWebServiceRequest.class, RequestMeta.class);
RequestMeta requestMeta = contextStore.get(request);
if (requestMeta == null) {
requestMeta = new RequestMeta();
contextStore.put(request, requestMeta);
}
requestMeta.setStreamName(value);
}
}
public static class TableNameAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void methodEnter(
@Advice.Argument(0) String value, @Advice.This AmazonWebServiceRequest request) {
ContextStore<AmazonWebServiceRequest, RequestMeta> contextStore =
InstrumentationContext.get(AmazonWebServiceRequest.class, RequestMeta.class);
RequestMeta requestMeta = contextStore.get(request);
if (requestMeta == null) {
requestMeta = new RequestMeta();
contextStore.put(request, requestMeta);
}
requestMeta.setTableName(value);
}
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11;
import com.amazonaws.handlers.HandlerContextKey;
import io.opentelemetry.javaagent.instrumentation.api.SpanWithScope;
import java.util.Objects;
public class RequestMeta {
// Note: aws1.x sdk doesn't have any truly async clients so we can store scope in request context
// safely.
public static final HandlerContextKey<SpanWithScope> SPAN_SCOPE_PAIR_CONTEXT_KEY =
new HandlerContextKey<>("io.opentelemetry.auto.SpanWithScope");
private String bucketName;
private String queueUrl;
private String queueName;
private String streamName;
private String tableName;
public String getBucketName() {
return bucketName;
}
public void setBucketName(String bucketName) {
this.bucketName = bucketName;
}
public String getQueueUrl() {
return queueUrl;
}
public void setQueueUrl(String queueUrl) {
this.queueUrl = queueUrl;
}
public String getQueueName() {
return queueName;
}
public void setQueueName(String queueName) {
this.queueName = queueName;
}
public String getStreamName() {
return streamName;
}
public void setStreamName(String streamName) {
this.streamName = streamName;
}
public String getTableName() {
return tableName;
}
public void setTableName(String tableName) {
this.tableName = tableName;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof RequestMeta)) {
return false;
}
RequestMeta other = (RequestMeta) obj;
return Objects.equals(bucketName, other.bucketName)
&& Objects.equals(queueUrl, other.queueUrl)
&& Objects.equals(queueName, other.queueName)
&& Objects.equals(streamName, other.streamName)
&& Objects.equals(tableName, other.tableName);
}
@Override
public int hashCode() {
return Objects.hash(bucketName, queueUrl, queueName, streamName, tableName);
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11;
import static io.opentelemetry.javaagent.instrumentation.awssdk.v1_11.AwsSdkClientTracer.tracer;
import static io.opentelemetry.javaagent.instrumentation.awssdk.v1_11.RequestMeta.SPAN_SCOPE_PAIR_CONTEXT_KEY;
import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.Request;
import com.amazonaws.Response;
import com.amazonaws.handlers.RequestHandler2;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.instrumentation.api.ContextStore;
import io.opentelemetry.javaagent.instrumentation.api.SpanWithScope;
/** Tracing Request Handler. */
public class TracingRequestHandler extends RequestHandler2 {
private final ContextStore<AmazonWebServiceRequest, RequestMeta> contextStore;
public TracingRequestHandler(ContextStore<AmazonWebServiceRequest, RequestMeta> contextStore) {
this.contextStore = contextStore;
}
@Override
public void beforeRequest(Request<?> request) {
AmazonWebServiceRequest originalRequest = request.getOriginalRequest();
RequestMeta requestMeta = contextStore.get(originalRequest);
Span span = tracer().startSpan(request, requestMeta);
Scope scope = tracer().startScope(span, request);
request.addHandlerContext(SPAN_SCOPE_PAIR_CONTEXT_KEY, new SpanWithScope(span, scope));
}
@Override
public void afterResponse(Request<?> request, Response<?> response) {
SpanWithScope scope = request.getHandlerContext(SPAN_SCOPE_PAIR_CONTEXT_KEY);
if (scope != null) {
request.addHandlerContext(SPAN_SCOPE_PAIR_CONTEXT_KEY, null);
scope.closeScope();
tracer().end(scope.getSpan(), response);
}
}
@Override
public void afterError(Request<?> request, Response<?> response, Exception e) {
SpanWithScope scope = request.getHandlerContext(SPAN_SCOPE_PAIR_CONTEXT_KEY);
if (scope != null) {
request.addHandlerContext(SPAN_SCOPE_PAIR_CONTEXT_KEY, null);
scope.closeScope();
tracer().endExceptionally(scope.getSpan(), response, e);
}
}
}

View File

@ -0,0 +1,342 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import static io.opentelemetry.api.trace.Span.Kind.CLIENT
import static io.opentelemetry.instrumentation.test.server.http.TestHttpServer.httpServer
import static io.opentelemetry.instrumentation.test.utils.PortUtils.UNUSABLE_PORT
import com.amazonaws.AmazonClientException
import com.amazonaws.AmazonWebServiceClient
import com.amazonaws.ClientConfiguration
import com.amazonaws.Request
import com.amazonaws.SDKGlobalConfiguration
import com.amazonaws.SdkClientException
import com.amazonaws.auth.AWSCredentialsProviderChain
import com.amazonaws.auth.AWSStaticCredentialsProvider
import com.amazonaws.auth.AnonymousAWSCredentials
import com.amazonaws.auth.BasicAWSCredentials
import com.amazonaws.auth.EnvironmentVariableCredentialsProvider
import com.amazonaws.auth.InstanceProfileCredentialsProvider
import com.amazonaws.auth.SystemPropertiesCredentialsProvider
import com.amazonaws.auth.profile.ProfileCredentialsProvider
import com.amazonaws.client.builder.AwsClientBuilder
import com.amazonaws.handlers.RequestHandler2
import com.amazonaws.regions.Regions
import com.amazonaws.retry.PredefinedRetryPolicies
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder
import com.amazonaws.services.dynamodbv2.model.CreateTableRequest
import com.amazonaws.services.ec2.AmazonEC2ClientBuilder
import com.amazonaws.services.kinesis.AmazonKinesisClientBuilder
import com.amazonaws.services.kinesis.model.DeleteStreamRequest
import com.amazonaws.services.rds.AmazonRDSClientBuilder
import com.amazonaws.services.rds.model.DeleteOptionGroupRequest
import com.amazonaws.services.s3.AmazonS3Client
import com.amazonaws.services.s3.AmazonS3ClientBuilder
import com.amazonaws.services.sqs.AmazonSQSClientBuilder
import com.amazonaws.services.sqs.model.CreateQueueRequest
import com.amazonaws.services.sqs.model.SendMessageRequest
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.attributes.SemanticAttributes
import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer
import io.opentelemetry.instrumentation.test.AgentTestRunner
import java.util.concurrent.atomic.AtomicReference
import spock.lang.AutoCleanup
import spock.lang.Shared
class Aws1ClientTest extends AgentTestRunner {
private static final CREDENTIALS_PROVIDER_CHAIN = new AWSCredentialsProviderChain(
new EnvironmentVariableCredentialsProvider(),
new SystemPropertiesCredentialsProvider(),
new ProfileCredentialsProvider(),
new InstanceProfileCredentialsProvider())
def setupSpec() {
System.setProperty(SDKGlobalConfiguration.ACCESS_KEY_SYSTEM_PROPERTY, "my-access-key")
System.setProperty(SDKGlobalConfiguration.SECRET_KEY_SYSTEM_PROPERTY, "my-secret-key")
}
def cleanupSpec() {
System.clearProperty(SDKGlobalConfiguration.ACCESS_KEY_SYSTEM_PROPERTY)
System.clearProperty(SDKGlobalConfiguration.SECRET_KEY_SYSTEM_PROPERTY)
}
@Shared
def credentialsProvider = new AWSStaticCredentialsProvider(new AnonymousAWSCredentials())
@Shared
def responseBody = new AtomicReference<String>()
@AutoCleanup
@Shared
def server = httpServer {
handlers {
all {
response.status(200).send(responseBody.get())
}
}
}
@Shared
def endpoint = new AwsClientBuilder.EndpointConfiguration("http://localhost:$server.address.port", "us-west-2")
def "request handler is hooked up with builder"() {
setup:
def builder = AmazonS3ClientBuilder.standard()
.withRegion(Regions.US_EAST_1)
if (addHandler) {
builder.withRequestHandlers(new RequestHandler2() {})
}
AmazonWebServiceClient client = builder.build()
expect:
client.requestHandler2s != null
client.requestHandler2s.size() == size
client.requestHandler2s.get(position).getClass().getSimpleName() == "TracingRequestHandler"
where:
addHandler | size | position
true | 2 | 1
false | 1 | 0
}
def "request handler is hooked up with constructor"() {
setup:
String accessKey = "asdf"
String secretKey = "qwerty"
def credentials = new BasicAWSCredentials(accessKey, secretKey)
def client = new AmazonS3Client(credentials)
if (addHandler) {
client.addRequestHandler(new RequestHandler2() {})
}
expect:
client.requestHandler2s != null
client.requestHandler2s.size() == size
client.requestHandler2s.get(0).getClass().getSimpleName() == "TracingRequestHandler"
where:
addHandler | size
true | 2
false | 1
}
def "send #operation request with mocked response"() {
setup:
responseBody.set(body)
when:
def response = call.call(client)
then:
response != null
client.requestHandler2s != null
client.requestHandler2s.size() == handlerCount
client.requestHandler2s.get(0).getClass().getSimpleName() == "TracingRequestHandler"
assertTraces(1) {
trace(0, 1) {
span(0) {
name "$service.$operation"
kind CLIENT
errored false
hasNoParent()
attributes {
"${SemanticAttributes.NET_TRANSPORT.key()}" "IP.TCP"
"${SemanticAttributes.HTTP_URL.key()}" "$server.address"
"${SemanticAttributes.HTTP_METHOD.key()}" "$method"
"${SemanticAttributes.HTTP_STATUS_CODE.key()}" 200
"${SemanticAttributes.HTTP_FLAVOR.key()}" "1.1"
"${SemanticAttributes.NET_PEER_PORT.key()}" server.address.port
"${SemanticAttributes.NET_PEER_NAME.key()}" "localhost"
"aws.service" { it.contains(service) }
"aws.endpoint" "$server.address"
"aws.operation" "${operation}Request"
"aws.agent" "java-aws-sdk"
for (def addedTag : additionalAttributes) {
"$addedTag.key" "$addedTag.value"
}
}
}
}
}
server.lastRequest.headers.get("traceparent") != null
where:
service | operation | method | path | handlerCount | client | call | additionalAttributes | body
"S3" | "CreateBucket" | "PUT" | "/testbucket/" | 1 | AmazonS3ClientBuilder.standard().withPathStyleAccessEnabled(true).withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { client -> client.createBucket("testbucket") } | ["aws.bucket.name": "testbucket"] | ""
"S3" | "GetObject" | "GET" | "/someBucket/someKey" | 1 | AmazonS3ClientBuilder.standard().withPathStyleAccessEnabled(true).withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { client -> client.getObject("someBucket", "someKey") } | ["aws.bucket.name": "someBucket"] | ""
"DynamoDBv2" | "CreateTable" | "POST" | "/" | 1 | AmazonDynamoDBClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.createTable(new CreateTableRequest("sometable", null)) } | ["aws.table.name": "sometable"] | ""
"Kinesis" | "DeleteStream" | "POST" | "/" | 1 | AmazonKinesisClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.deleteStream(new DeleteStreamRequest().withStreamName("somestream")) } | ["aws.stream.name": "somestream"] | ""
"SQS" | "CreateQueue" | "POST" | "/" | 4 | AmazonSQSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.createQueue(new CreateQueueRequest("somequeue")) } | ["aws.queue.name": "somequeue"] | """
<CreateQueueResponse>
<CreateQueueResult><QueueUrl>https://queue.amazonaws.com/123456789012/MyQueue</QueueUrl></CreateQueueResult>
<ResponseMetadata><RequestId>7a62c49f-347e-4fc4-9331-6e8e7a96aa73</RequestId></ResponseMetadata>
</CreateQueueResponse>
"""
"SQS" | "SendMessage" | "POST" | "/someurl" | 4 | AmazonSQSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.sendMessage(new SendMessageRequest("someurl", "")) } | ["aws.queue.url": "someurl"] | """
<SendMessageResponse>
<SendMessageResult>
<MD5OfMessageBody>d41d8cd98f00b204e9800998ecf8427e</MD5OfMessageBody>
<MD5OfMessageAttributes>3ae8f24a165a8cedc005670c81a27295</MD5OfMessageAttributes>
<MessageId>5fea7756-0ea4-451a-a703-a558b933e274</MessageId>
</SendMessageResult>
<ResponseMetadata><RequestId>27daac76-34dd-47df-bd01-1f6e873584a0</RequestId></ResponseMetadata>
</SendMessageResponse>
"""
"EC2" | "AllocateAddress" | "POST" | "/" | 4 | AmazonEC2ClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { client -> client.allocateAddress() } | [:] | """
<AllocateAddressResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<publicIp>192.0.2.1</publicIp>
<domain>standard</domain>
</AllocateAddressResponse>
"""
"RDS" | "DeleteOptionGroup" | "POST" | "/" | 5 | AmazonRDSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { client -> client.deleteOptionGroup(new DeleteOptionGroupRequest()) } | [:] | """
<DeleteOptionGroupResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/">
<ResponseMetadata>
<RequestId>0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99</RequestId>
</ResponseMetadata>
</DeleteOptionGroupResponse>
"""
}
def "send #operation request to closed port"() {
setup:
responseBody.set(body)
when:
call.call(client)
then:
thrown SdkClientException
assertTraces(1) {
trace(0, 1) {
span(0) {
name "$service.$operation"
kind CLIENT
errored true
errorEvent SdkClientException, ~/Unable to execute HTTP request/
hasNoParent()
attributes {
"${SemanticAttributes.NET_TRANSPORT.key()}" "IP.TCP"
"${SemanticAttributes.HTTP_URL.key()}" "http://localhost:${UNUSABLE_PORT}"
"${SemanticAttributes.HTTP_METHOD.key()}" "$method"
"${SemanticAttributes.HTTP_FLAVOR.key()}" "1.1"
"${SemanticAttributes.NET_PEER_NAME.key()}" "localhost"
"${SemanticAttributes.NET_PEER_PORT.key()}" 61
"aws.service" { it.contains(service) }
"aws.endpoint" "http://localhost:${UNUSABLE_PORT}"
"aws.operation" "${operation}Request"
"aws.agent" "java-aws-sdk"
for (def addedTag : additionalAttributes) {
"$addedTag.key" "$addedTag.value"
}
}
}
}
}
where:
service | operation | method | url | call | additionalAttributes | body | client
"S3" | "GetObject" | "GET" | "someBucket/someKey" | { client -> client.getObject("someBucket", "someKey") } | ["aws.bucket.name": "someBucket"] | "" | new AmazonS3Client(CREDENTIALS_PROVIDER_CHAIN, new ClientConfiguration().withRetryPolicy(PredefinedRetryPolicies.getDefaultRetryPolicyWithCustomMaxRetries(0))).withEndpoint("http://localhost:${UNUSABLE_PORT}")
}
def "naughty request handler doesn't break the trace"() {
setup:
def client = new AmazonS3Client(CREDENTIALS_PROVIDER_CHAIN)
client.addRequestHandler(new RequestHandler2() {
void beforeRequest(Request<?> request) {
throw new RuntimeException("bad handler")
}
})
when:
client.getObject("someBucket", "someKey")
then:
!Span.current().getSpanContext().isValid()
thrown RuntimeException
assertTraces(1) {
trace(0, 1) {
span(0) {
name "S3.HeadBucket"
kind CLIENT
errored true
errorEvent RuntimeException, "bad handler"
hasNoParent()
attributes {
"${SemanticAttributes.NET_TRANSPORT.key()}" "IP.TCP"
"${SemanticAttributes.HTTP_URL.key()}" "https://s3.amazonaws.com"
"${SemanticAttributes.HTTP_METHOD.key()}" "HEAD"
"${SemanticAttributes.HTTP_FLAVOR.key()}" "1.1"
"${SemanticAttributes.NET_PEER_NAME.key()}" "s3.amazonaws.com"
"aws.service" "Amazon S3"
"aws.endpoint" "https://s3.amazonaws.com"
"aws.operation" "HeadBucketRequest"
"aws.agent" "java-aws-sdk"
}
}
}
}
}
// TODO(anuraaga): Add events for retries.
def "timeout and retry errors not captured"() {
setup:
def server = httpServer {
handlers {
all {
Thread.sleep(500)
response.status(200).send()
}
}
}
AmazonS3Client client = new AmazonS3Client(new ClientConfiguration().withRequestTimeout(50 /* ms */))
.withEndpoint("http://localhost:$server.address.port")
when:
client.getObject("someBucket", "someKey")
then:
!Span.current().getSpanContext().isValid()
thrown AmazonClientException
assertTraces(1) {
trace(0, 1) {
span(0) {
name "S3.GetObject"
kind CLIENT
errored true
try {
errorEvent AmazonClientException, ~/Unable to execute HTTP request/
} catch (AssertionError e) {
errorEvent SdkClientException, "Unable to execute HTTP request: Request did not complete before the request timeout configuration."
}
hasNoParent()
attributes {
"${SemanticAttributes.NET_TRANSPORT.key()}" "IP.TCP"
"${SemanticAttributes.HTTP_URL.key()}" "$server.address"
"${SemanticAttributes.HTTP_METHOD.key()}" "GET"
"${SemanticAttributes.NET_PEER_PORT.key()}" server.address.port
"${SemanticAttributes.NET_PEER_NAME.key()}" "localhost"
"${SemanticAttributes.HTTP_FLAVOR.key()}" "1.1"
"aws.service" "Amazon S3"
"aws.endpoint" "$server.address"
"aws.operation" "GetObjectRequest"
"aws.agent" "java-aws-sdk"
"aws.bucket.name" "someBucket"
}
}
}
}
cleanup:
server.close()
}
String expectedOperationName(String method) {
return method != null ? "HTTP $method" : HttpClientTracer.DEFAULT_SPAN_NAME
}
}

View File

@ -0,0 +1,284 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import static io.opentelemetry.api.trace.Span.Kind.CLIENT
import static io.opentelemetry.instrumentation.test.server.http.TestHttpServer.httpServer
import static io.opentelemetry.instrumentation.test.utils.PortUtils.UNUSABLE_PORT
import com.amazonaws.AmazonClientException
import com.amazonaws.ClientConfiguration
import com.amazonaws.Request
import com.amazonaws.SDKGlobalConfiguration
import com.amazonaws.auth.AWSCredentialsProviderChain
import com.amazonaws.auth.BasicAWSCredentials
import com.amazonaws.auth.EnvironmentVariableCredentialsProvider
import com.amazonaws.auth.InstanceProfileCredentialsProvider
import com.amazonaws.auth.SystemPropertiesCredentialsProvider
import com.amazonaws.auth.profile.ProfileCredentialsProvider
import com.amazonaws.handlers.RequestHandler2
import com.amazonaws.retry.PredefinedRetryPolicies
import com.amazonaws.services.ec2.AmazonEC2Client
import com.amazonaws.services.rds.AmazonRDSClient
import com.amazonaws.services.rds.model.DeleteOptionGroupRequest
import com.amazonaws.services.s3.AmazonS3Client
import com.amazonaws.services.s3.S3ClientOptions
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.attributes.SemanticAttributes
import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer
import io.opentelemetry.instrumentation.test.AgentTestRunner
import java.util.concurrent.atomic.AtomicReference
import spock.lang.AutoCleanup
import spock.lang.Shared
class Aws0ClientTest extends AgentTestRunner {
private static final CREDENTIALS_PROVIDER_CHAIN = new AWSCredentialsProviderChain(
new EnvironmentVariableCredentialsProvider(),
new SystemPropertiesCredentialsProvider(),
new ProfileCredentialsProvider(),
new InstanceProfileCredentialsProvider())
def setupSpec() {
System.setProperty(SDKGlobalConfiguration.ACCESS_KEY_SYSTEM_PROPERTY, "my-access-key")
System.setProperty(SDKGlobalConfiguration.SECRET_KEY_SYSTEM_PROPERTY, "my-secret-key")
}
def cleanupSpec() {
System.clearProperty(SDKGlobalConfiguration.ACCESS_KEY_SYSTEM_PROPERTY)
System.clearProperty(SDKGlobalConfiguration.SECRET_KEY_SYSTEM_PROPERTY)
}
@Shared
def responseBody = new AtomicReference<String>()
@AutoCleanup
@Shared
def server = httpServer {
handlers {
all {
response.status(200).send(responseBody.get())
}
}
}
def "request handler is hooked up with constructor"() {
setup:
String accessKey = "asdf"
String secretKey = "qwerty"
def credentials = new BasicAWSCredentials(accessKey, secretKey)
def client = new AmazonS3Client(credentials)
if (addHandler) {
client.addRequestHandler(new RequestHandler2() {})
}
expect:
client.requestHandler2s != null
client.requestHandler2s.size() == size
client.requestHandler2s.get(0).getClass().getSimpleName() == "TracingRequestHandler"
where:
addHandler | size
true | 2
false | 1
}
def "send #operation request with mocked response"() {
setup:
responseBody.set(body)
when:
def response = call.call(client)
then:
response != null
client.requestHandler2s != null
client.requestHandler2s.size() == handlerCount
client.requestHandler2s.get(0).getClass().getSimpleName() == "TracingRequestHandler"
assertTraces(1) {
trace(0, 1) {
span(0) {
name "$service.$operation"
kind CLIENT
errored false
hasNoParent()
attributes {
"${SemanticAttributes.NET_TRANSPORT.key()}" "IP.TCP"
"${SemanticAttributes.HTTP_URL.key()}" "$server.address"
"${SemanticAttributes.HTTP_METHOD.key()}" "$method"
"${SemanticAttributes.HTTP_STATUS_CODE.key()}" 200
"${SemanticAttributes.HTTP_FLAVOR.key()}" "1.1"
"${SemanticAttributes.NET_PEER_PORT.key()}" server.address.port
"${SemanticAttributes.NET_PEER_NAME.key()}" "localhost"
"aws.service" { it.contains(service) }
"aws.endpoint" "$server.address"
"aws.operation" "${operation}Request"
"aws.agent" "java-aws-sdk"
for (def addedTag : additionalAttributes) {
"$addedTag.key" "$addedTag.value"
}
}
}
}
}
server.lastRequest.headers.get("traceparent") != null
where:
service | operation | method | path | handlerCount | client | additionalAttributes | call | body
"S3" | "CreateBucket" | "PUT" | "/testbucket/" | 1 | new AmazonS3Client().withEndpoint("http://localhost:$server.address.port") | ["aws.bucket.name": "testbucket"] | { client -> client.setS3ClientOptions(S3ClientOptions.builder().setPathStyleAccess(true).build()); client.createBucket("testbucket") } | ""
"S3" | "GetObject" | "GET" | "/someBucket/someKey" | 1 | new AmazonS3Client().withEndpoint("http://localhost:$server.address.port") | ["aws.bucket.name": "someBucket"] | { client -> client.getObject("someBucket", "someKey") } | ""
"EC2" | "AllocateAddress" | "POST" | "/" | 4 | new AmazonEC2Client().withEndpoint("http://localhost:$server.address.port") | [:] | { client -> client.allocateAddress() } | """
<AllocateAddressResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<publicIp>192.0.2.1</publicIp>
<domain>standard</domain>
</AllocateAddressResponse>
"""
"RDS" | "DeleteOptionGroup" | "POST" | "/" | 1 | new AmazonRDSClient().withEndpoint("http://localhost:$server.address.port") | [:] | { client -> client.deleteOptionGroup(new DeleteOptionGroupRequest()) } | """
<DeleteOptionGroupResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/">
<ResponseMetadata>
<RequestId>0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99</RequestId>
</ResponseMetadata>
</DeleteOptionGroupResponse>
"""
}
def "send #operation request to closed port"() {
setup:
responseBody.set(body)
when:
call.call(client)
then:
thrown AmazonClientException
assertTraces(1) {
trace(0, 1) {
span(0) {
name "$service.$operation"
kind CLIENT
errored true
errorEvent AmazonClientException, ~/Unable to execute HTTP request/
hasNoParent()
attributes {
"${SemanticAttributes.NET_TRANSPORT.key()}" "IP.TCP"
"${SemanticAttributes.HTTP_URL.key()}" "http://localhost:${UNUSABLE_PORT}"
"${SemanticAttributes.HTTP_METHOD.key()}" "$method"
"${SemanticAttributes.HTTP_FLAVOR.key()}" "1.1"
"${SemanticAttributes.NET_PEER_PORT.key()}" 61
"${SemanticAttributes.NET_PEER_NAME.key()}" "localhost"
"aws.service" { it.contains(service) }
"aws.endpoint" "http://localhost:${UNUSABLE_PORT}"
"aws.operation" "${operation}Request"
"aws.agent" "java-aws-sdk"
for (def addedTag : additionalAttributes) {
"$addedTag.key" "$addedTag.value"
}
}
}
}
}
where:
service | operation | method | url | call | additionalAttributes | body | client
"S3" | "GetObject" | "GET" | "someBucket/someKey" | { client -> client.getObject("someBucket", "someKey") } | ["aws.bucket.name": "someBucket"] | "" | new AmazonS3Client(CREDENTIALS_PROVIDER_CHAIN, new ClientConfiguration().withRetryPolicy(PredefinedRetryPolicies.getDefaultRetryPolicyWithCustomMaxRetries(0))).withEndpoint("http://localhost:${UNUSABLE_PORT}")
}
def "naughty request handler doesn't break the trace"() {
setup:
def client = new AmazonS3Client(CREDENTIALS_PROVIDER_CHAIN)
client.addRequestHandler(new RequestHandler2() {
void beforeRequest(Request<?> request) {
throw new RuntimeException("bad handler")
}
})
when:
client.getObject("someBucket", "someKey")
then:
!Span.current().getSpanContext().isValid()
thrown RuntimeException
assertTraces(1) {
trace(0, 1) {
span(0) {
name "S3.GetObject"
kind CLIENT
errored true
errorEvent RuntimeException, "bad handler"
hasNoParent()
attributes {
"${SemanticAttributes.NET_TRANSPORT.key()}" "IP.TCP"
"${SemanticAttributes.HTTP_URL.key()}" "https://s3.amazonaws.com"
"${SemanticAttributes.HTTP_METHOD.key()}" "GET"
"${SemanticAttributes.HTTP_FLAVOR.key()}" "1.1"
"${SemanticAttributes.NET_PEER_NAME.key()}" "s3.amazonaws.com"
"aws.service" "Amazon S3"
"aws.endpoint" "https://s3.amazonaws.com"
"aws.operation" "GetObjectRequest"
"aws.agent" "java-aws-sdk"
"aws.bucket.name" "someBucket"
}
}
}
}
}
// TODO(anuraaga): Add events for retries.
def "timeout and retry errors not captured"() {
setup:
def server = httpServer {
handlers {
all {
Thread.sleep(500)
response.status(200).send()
}
}
}
AmazonS3Client client = new AmazonS3Client(new ClientConfiguration().withRequestTimeout(50 /* ms */))
.withEndpoint("http://localhost:$server.address.port")
when:
client.getObject("someBucket", "someKey")
then:
!Span.current().getSpanContext().isValid()
thrown AmazonClientException
assertTraces(1) {
trace(0, 1) {
span(0) {
name "S3.GetObject"
kind CLIENT
errored true
errorEvent AmazonClientException, ~/Unable to execute HTTP request/
hasNoParent()
attributes {
"${SemanticAttributes.NET_TRANSPORT.key()}" "IP.TCP"
"${SemanticAttributes.HTTP_URL.key()}" "$server.address"
"${SemanticAttributes.HTTP_METHOD.key()}" "GET"
"${SemanticAttributes.HTTP_FLAVOR.key()}" "1.1"
"${SemanticAttributes.NET_PEER_PORT.key()}" server.address.port
"${SemanticAttributes.NET_PEER_NAME.key()}" "localhost"
"aws.service" "Amazon S3"
"aws.endpoint" "http://localhost:$server.address.port"
"aws.operation" "GetObjectRequest"
"aws.agent" "java-aws-sdk"
"aws.bucket.name" "someBucket"
}
}
}
}
cleanup:
server.close()
}
String expectedOperationName(String method) {
return method != null ? "HTTP $method" : HttpClientTracer.DEFAULT_SPAN_NAME
}
}

View File

@ -0,0 +1,43 @@
// Set properties before any plugins get loaded
ext {
cassandraDriverTestVersions = "[3.0,4.0)"
}
apply from: "$rootDir/gradle/instrumentation.gradle"
muzzle {
pass {
group = "com.datastax.cassandra"
module = "cassandra-driver-core"
versions = cassandraDriverTestVersions
assertInverse = true
// Older versions of cassandra-driver-core require an older guava dependency (0.16.0). guava >20.0 is not
// compatible with Java 7, so we declare the dependency on 20.0 in our top level dependencies.gradle.
// Ideally our muzzle plugin should take into account those versions declaration, instead it does not so we would
// end up with testing compatibility with guava 0.16 which lacks the method invocation added to be compatible with
// most recent versions of guava (27+). While the long term solution is to make the muzzle plugin aware of upstream
// declared dependencies, for now we just make sure that we use the proper ones.
extraDependency "com.google.guava:guava:20.0"
}
// Making sure that instrumentation works with recent versions of Guava which removed method
// Futures::transform(input, function) in favor of Futures::transform(input, function, executor)
pass {
name = "Newest versions of Guava"
group = "com.datastax.cassandra"
module = "cassandra-driver-core"
versions = cassandraDriverTestVersions
// While com.datastax.cassandra uses old versions of Guava, users may depends themselves on newer versions of Guava
extraDependency "com.google.guava:guava:27.0-jre"
}
}
dependencies {
library group: 'com.datastax.cassandra', name: 'cassandra-driver-core', version: '3.0.0'
testLibrary group: 'com.datastax.cassandra', name: 'cassandra-driver-core', version: '3.2.0'
testImplementation project(':instrumentation:guava-10.0:javaagent')
latestDepTestLibrary group: 'com.datastax.cassandra', name: 'cassandra-driver-core', version: '3.+'
}

View File

@ -0,0 +1,70 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.cassandra.v3_0;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPrivate;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import com.datastax.driver.core.Session;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
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;
@AutoService(InstrumentationModule.class)
public class CassandraClientInstrumentationModule extends InstrumentationModule {
public CassandraClientInstrumentationModule() {
super("cassandra", "cassandra-3.0");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new ClusterManagerInstrumentation());
}
public static class ClusterManagerInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
// Note: Cassandra has a large driver and we instrument single class in it.
// The rest is ignored in AdditionalLibraryIgnoresMatcher
return named("com.datastax.driver.core.Cluster$Manager");
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
isMethod().and(isPrivate()).and(named("newSession")).and(takesArguments(0)),
CassandraClientInstrumentationModule.class.getName() + "$CassandraClientAdvice");
}
}
public static class CassandraClientAdvice {
/**
* Strategy: each time we build a connection to a Cassandra cluster, the
* com.datastax.driver.core.Cluster$Manager.newSession() method is called. The opentracing
* contribution is a simple wrapper, so we just have to wrap the new session.
*
* @param session The fresh session to patch. This session is replaced with new session
*/
@Advice.OnMethodExit(suppress = Throwable.class)
public static void injectTracingSession(@Advice.Return(readOnly = false) Session session) {
// This should cover ours and OT's TracingSession
if (session.getClass().getName().endsWith("cassandra.TracingSession")) {
return;
}
session = new TracingSession(session);
}
}
}

Some files were not shown because too many files have changed in this diff Show More