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:
parent
f2bb2f3e30
commit
5f263644da
|
@ -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'
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
akka.http {
|
||||
host-connection-pool {
|
||||
// Limit maximum http backoff for tests
|
||||
max-connection-backoff = 100ms
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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.+'
|
||||
}
|
|
@ -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 + "]";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}'")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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'
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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.+'
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 })
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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.+'
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue