Skip to content

Commit

Permalink
Updated POM files and unit test.
Browse files Browse the repository at this point in the history
Updated POM files to point to 2.4.0 of the X-Ray SDK. Updated unit test to reflect this change.
Updated POM files to point to DiSCo 0.9.1.
Clean and reorganize POM files.

--
Merge branch 'master' of https://github.com/chanchiem/aws-xray-java-agent into public_git
--

Initial release of the AWS X-Ray Auto Instrumentation Agent
* Allows auto instrumentation of existing Java applications.
* AWS SDK V1 Support
* Multi Threaded Support
* HttpClient Support
  • Loading branch information
chanchiem committed Nov 26, 2019
1 parent 11e33d2 commit 000b4af
Show file tree
Hide file tree
Showing 58 changed files with 6,238 additions and 0 deletions.
24 changes: 24 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Remove .DS_Store
.DS_Store

# Ignore Gradle project-specific cache directory
.gradle

# Ignore Gradle build output directory
build

# IDEA stuffs
*.iml
.idea

target/*
**/target/*
.gradle/*
**/.gradle/*
.settings/
.project
.classpath
build
.DS_Store
*.iml
/.idea
93 changes: 93 additions & 0 deletions aws-xray-auto-instrumentation-agent-bootstrap/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-auto-instrumentation-agent-pom</artifactId>
<version>2.4.0</version>
</parent>
<artifactId>aws-xray-auto-instrumentation-agent-bootstrap</artifactId>
<name>AWS X-Ray Auto Instrumentation Agent for Java - Bootstrap</name>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Premain-Class>software.amazon.disco.agent.XRayInstrumentationAgent</Premain-Class>
<Agent-Class>software.amazon.disco.agent.XRayInstrumentationAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Boot-Class-Path>${project.build.finalName}.jar</Boot-Class-Path>
</manifestEntries>
</transformer>
</transformers>
<relocations>
<relocation>
<pattern>org.objectweb.asm</pattern>
<shadedPattern>software.amazon.disco.agent.jar.asm</shadedPattern>
</relocation>
<relocation>
<pattern>com.fasterxml</pattern>
<shadedPattern>software.amazon.disco.agent.jar.fasterxmll</shadedPattern>
</relocation>
<relocation>
<pattern>net.bytebuddy</pattern>
<shadedPattern>software.amazon.disco.agent.jar.bytebuddy</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>software.amazon.disco</groupId>
<artifactId>disco-java-agent-core</artifactId>
<version>${disco.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.disco</groupId>
<artifactId>disco-java-agent-web</artifactId>
<version>${disco.version}</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk</artifactId>
<version>1.11.228</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package software.amazon.disco.agent;

/**
* Interface for the bridge between the agent runtime loader and the application class loader.
* This should be implemented in the application classloader.
*/
public interface AgentRuntimeLoaderInterface
{
// Maybe in the future convert serviceName into a configuration map
void init(String serviceName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package software.amazon.disco.agent;

import software.amazon.disco.agent.concurrent.ConcurrencySupport;
import software.amazon.disco.agent.event.EventBus;
import software.amazon.disco.agent.web.WebSupport;
import software.amazon.disco.agent.logging.LogManager;
import software.amazon.disco.agent.logging.Logger;
import software.amazon.disco.agent.interception.Installable;
import software.amazon.disco.agent.awsv1.AWSClientInvokeRecordInterceptor;
import software.amazon.disco.agent.awsv2.AWSClientBuilderInterceptor;

import java.lang.instrument.Instrumentation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.Set;

/**
* The entry point for the DiSCo Authority agent.
* For more on Java agents see: https://docs.oracle.com/javase/7/docs/api/java/lang/instrument/package-summary.html
*
* This class defines the 'premain' method to be executed at the time of class loading, before any loaded
* classes are actually used. At this time, the classes can be instrumented/transformed. The 'ByteBuddy' library
* is used to rewrite/rebase the intercepted classes.
*
* For more on ByteBuddy see: http://bytebuddy.net
*/
public class XRayInstrumentationAgent {
public static final String TRACE_HEADER_KEY = "X-Amzn-Trace-Id"; // TODO Merge this into a single file.
public static final String SERVICE_NAME_ARG = "servicename"; // TODO Merge this into a single file.
public static final String DEFAULT_SERVICE_NAME = "DefaultServiceName";

/**
* log4j logger for log messages.
*/
private static final Logger LOG = LogManager.getLogger(XRayInstrumentationAgent.class);

/**
* The agent is loaded by a -javaagent command line parameter, which will treat 'premain' as its
* entrypoint, in the class referenced by the Premain-Class attribute in the manifest - which should be this one.
*
* @param agentArgs - any arguments passed as part of the -javaagent argument string
* @param instrumentation - the Instrumentation object given to every Agent, to transform bytecode
*/
public static void premain(String agentArgs, Instrumentation instrumentation) {
LogManager.setMinimumLevel(Logger.Level.ERROR);

DiscoAgentTemplate discoAgentTemplate = new DiscoAgentTemplate(agentArgs);
Set<Installable> installables = new HashSet<>();

installables.addAll(new ConcurrencySupport().get());

// Required for intercepting HttpClients and Servlets
installables.addAll(new WebSupport().get());

// AWS SDK instrumentation
installables.add(new AWSClientInvokeRecordInterceptor()); // V1
installables.add(new AWSClientBuilderInterceptor()); // V2

// Note that we should not load any other classes before we install the interceptors. Many of the interceptors
// only work if they haven't been loaded by the JVM yet.
discoAgentTemplate.install(instrumentation, installables);

// Attempt to get the runtime loader; if this fails, we abort by removing the XRayListener from the event bus.
// Add XRay Listener to the event bus, parse out the agent name from the argument
// In a lambda environment, the instrumentation is constructed through a proxy class, so that the
// classloader would the customerClassloader; in a general case, the instrumentation would be instantiated
// at the bootstrap classloader, so we by default use the system classloader during those scenarios.
if (!initializeRuntimeAgent(agentArgs, instrumentation.getClass().getClassLoader())) {
LOG.error("Unable to initialize the runtime agent. Running without instrumentation.");
EventBus.removeAllListeners();
return;
}
}

private static boolean initializeRuntimeAgent(String agentArgs, ClassLoader classLoader) {
// Reflectively acquire the agent runtime loader and initialize it to configure X-Ray.
try {
AgentRuntimeLoaderInterface agentRuntimeLoader = getAgentRuntimeLoader(classLoader);
String serviceName = getServiceNameFromArgs(agentArgs, DEFAULT_SERVICE_NAME);
agentRuntimeLoader.init(serviceName);
return true;
} catch (ClassNotFoundException e) {
LOG.error("Unable to locate agent runtime loader. Please make sure it's imported as a dependency.");
} catch (NoSuchMethodException e) {
LOG.error("Unable to locate the agent runtime loader constructor.");
} catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
LOG.error("Unable to initialize the agent runtime loader.");
}
return false;
}

private static AgentRuntimeLoaderInterface getAgentRuntimeLoader(ClassLoader classLoader) throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
// Iterate through its parents until we find it. If we reach the bootstrap, then we know it doesn't exist.

Class<?> agentRuntimeLoaderClass = null;
ClassLoader currentClassloader = classLoader;

// Use the application classloader if the classloader passed in is null (within the bootstrap)
if (currentClassloader == null) {
currentClassloader = ClassLoader.getSystemClassLoader();
}
agentRuntimeLoaderClass = Class.forName("com.amazonaws.xray.agent.AgentRuntimeLoader", true, currentClassloader);

Constructor runtimeLoaderConstructor = agentRuntimeLoaderClass.getConstructor();
return (AgentRuntimeLoaderInterface) runtimeLoaderConstructor.newInstance();
}

private static String getServiceNameFromArgs(String agentArgs, String defaultName) {
// *VERY* crude method for obtaining just the service name.
// Should probably write or find an arg parser to do more complicated stuff.
if (agentArgs == null) {
return defaultName;
}

String[] argArray = agentArgs.split("=");
for(int i = 0; i < argArray.length-1; i++) {
String argOrValue = argArray[i];
if (argOrValue.contentEquals(SERVICE_NAME_ARG)) {
return argArray[i+1];
}
}
return defaultName;
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package software.amazon.disco.agent.awsv1;

import software.amazon.disco.agent.event.EventBus;
import software.amazon.disco.agent.event.ServiceDownstreamRequestEvent;
import software.amazon.disco.agent.event.ServiceDownstreamResponseEvent;
import software.amazon.disco.agent.interception.Installable;
import software.amazon.disco.agent.logging.LogManager;
import software.amazon.disco.agent.logging.Logger;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.Argument;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;

import java.util.concurrent.Callable;

/**
* When making a downstream AWS call, the doInvoke method is intercepted.
*/
public class AWSClientInvokeInterceptor implements Installable {
private static Logger log = LogManager.getLogger(AWSClientInvokeInterceptor.class);

/**
* This method is used to replace the doInvoke() method inside each of the AWS clients.
*
* @param request - The method signature of the intercepted call is:
* Response (DefaultRequest, HttpResponseHandler, ExecutionContext
* The first argument is the request object which is the argument we need.
* @param origin - an identifier of the intercepted Method, for logging/debugging
* @param zuper - ByteBuddy supplies a Callable to the intercepted method, due to the @SuperCall annotation
* @return - The object produced by the original intercepted request
* @throws Exception - The internal call to 'zuper.call()' may throw any Exception
*/
@SuppressWarnings("unused")
@RuntimeType
public static Object doInvoke(@Argument(0) Object request,
@Origin String origin,
@SuperCall Callable<Object> zuper) throws Throwable {
log.debug("DiSCo(AWS) method interception of " + origin);

//Retrieve name of AWS service we are calling
String serviceName = (String) request.getClass().getMethod("getServiceName").invoke(request);

//Original request contains both the operation name and the request object
Object originalRequest = request.getClass().getMethod("getOriginalRequest").invoke(request);
String operationName = originalRequest.getClass().getSimpleName();
operationName = operationName.replace("Request", "");

ServiceDownstreamRequestEvent requestEvent = new ServiceDownstreamRequestEvent("AWS", serviceName, operationName);
requestEvent.withRequest(request);
EventBus.publish(requestEvent);

//make the original call, and wrap in an exception catch, in case it throws instead of servicing the request
//normally.
Object output = null;
Throwable thrown = null;
try {
output = zuper.call();
} catch (Throwable t) {
thrown = t;
}

log.debug("DiSCo(AWS) publishing event from "+serviceName+"."+operationName);

ServiceDownstreamResponseEvent responseEvent = new ServiceDownstreamResponseEvent("AWS", serviceName, operationName, requestEvent);
responseEvent.withResponse(output);
responseEvent.withThrown(thrown);
EventBus.publish(responseEvent);

if (thrown != null) {
throw thrown;
}

return output;
}

/**
* {@inheritDoc}
*/
@Override
public AgentBuilder install(AgentBuilder agentBuilder) {
return agentBuilder
.type(buildClassMatcher())
.transform((builder, typeDescription, classLoader, module) -> builder
.method(buildMethodMatcher())
.intercept(MethodDelegation.to(AWSClientInvokeInterceptor.class)));

}

/**
* Builds a class matcher to discover all implemented
* AWS clients.
* @return an ElementMatcher suitable for passing to the type() method of a AgentBuilder
*/
ElementMatcher<? super TypeDescription> buildClassMatcher() {
return ElementMatchers.hasSuperType(ElementMatchers.named("com.amazonaws.AmazonWebServiceClient"))
.and(ElementMatchers.not(ElementMatchers.isAbstract()));
}

/**
* Builds a method matcher to match against all doInvoke methods in AWS clients
* @return an ElementMatcher suitable for passing to builder.method()
*/
ElementMatcher<? super MethodDescription> buildMethodMatcher() {
return ElementMatchers.named("doInvoke")
.and(ElementMatchers.not(ElementMatchers.isAbstract()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package software.amazon.disco.agent.awsv1;

/**
* Deprecated class maintaining the no-longer-used 'Record' naming convention. Kept for compatibility
* with agents still using it, but will be removed in a future version.
*/
@Deprecated
public class AWSClientInvokeRecordInterceptor extends AWSClientInvokeInterceptor {}
Loading

0 comments on commit 000b4af

Please sign in to comment.