diff --git a/dd-java-agent/instrumentation/axway-api/axway-api.gradle b/dd-java-agent/instrumentation/axway-api/axway-api.gradle new file mode 100644 index 00000000000..8f3b1c3c141 --- /dev/null +++ b/dd-java-agent/instrumentation/axway-api/axway-api.gradle @@ -0,0 +1,13 @@ +apply from: "$rootDir/gradle/java.gradle" + +dependencies { + // It's difficult to split jars from axway apigateway monolith: + // Jars are mostly frontend to native code and this native code doesn't run separately without licence. + // Moreover, there is no component which you can run `java -jar ...` nor add it to classpath. + // Attempt to load any Axway class it results in segmentation fault. + + //compileOnly group: 'com.axway.apigw', name: 'com.axway.apigw', version: '7.5' + //latestDepTestCompile group: 'com.axway.apigw', name: 'com.axway.apigw', version: '+' + + //testCompile group: 'com.axway.ats.framework', name: 'ats-core', version: '4.0.6' +} diff --git a/dd-java-agent/instrumentation/axway-api/src/main/java/datadog/trace/instrumentation/axway/AxwayHTTPPluginDecorator.java b/dd-java-agent/instrumentation/axway-api/src/main/java/datadog/trace/instrumentation/axway/AxwayHTTPPluginDecorator.java new file mode 100644 index 00000000000..be4a956e14d --- /dev/null +++ b/dd-java-agent/instrumentation/axway-api/src/main/java/datadog/trace/instrumentation/axway/AxwayHTTPPluginDecorator.java @@ -0,0 +1,236 @@ +package datadog.trace.instrumentation.axway; + +import static java.lang.invoke.MethodType.methodType; + +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.DefaultURIDataAdapter; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.URI; +import lombok.extern.slf4j.Slf4j; + +// request = is com.vordel.circuit.net.State, connection = com.vordel.dwe.http.ServerTransaction +@Slf4j +public class AxwayHTTPPluginDecorator extends HttpServerDecorator { + public static final CharSequence AXWAY_REQUEST = UTF8BytesString.create("axway.request"); + public static final CharSequence AXWAY_TRY_TRANSACTION = + UTF8BytesString.create("axway.trytransaction"); + + public static final AxwayHTTPPluginDecorator DECORATE = new AxwayHTTPPluginDecorator(); + + private static final MethodHandles.Lookup lookup = MethodHandles.lookup(); + + private static final String SERVERTRANSACTION_CLASSNAME = "com.vordel.dwe.http.ServerTransaction"; + private static final Class classServerTransaction; + private static final MethodHandle getRemoteAddr_mh; + private static final MethodHandle getMethod_mh; + private static final MethodHandle getURI_mh; + + private static final String STATE_CLASSNAME = "com.vordel.circuit.net.State"; + private static final Class classState; + private static final MethodHandle hostField_mh; + private static final MethodHandle portField_mh; + private static final MethodHandle methodField_mh; + private static final MethodHandle uriField_mh; + + static final String SERVER_TRANSACTION_CLASSNAME = "com.vordel.dwe.http.ServerTransaction"; + static final Class SERVER_TRANSACTION_CLASS = getServerTransactionClass(); + + private static Class getServerTransactionClass() { + try { + return (Class) Class.forName(SERVER_TRANSACTION_CLASSNAME); + } catch (ClassNotFoundException e) { + log.debug("Can't get ServerTransaction class name", e); + } + return null; + } + + static { + classServerTransaction = initClass(SERVERTRANSACTION_CLASSNAME); + getRemoteAddr_mh = + initNoArgServerTransactionMethodHandle("getRemoteAddr", InetSocketAddress.class); + getMethod_mh = initNoArgServerTransactionMethodHandle("getMethod", String.class); + getURI_mh = initGetURI(); + + classState = initClass(STATE_CLASSNAME); + hostField_mh = initStateFieldGetter("host"); + portField_mh = initStateFieldGetter("port"); + methodField_mh = initStateFieldGetter("verb"); + uriField_mh = initStateFieldGetter("uri"); + } + + private static Class initClass(final String name) { + try { + return Class.forName(name, false, AxwayHTTPPluginDecorator.class.getClassLoader()); + } catch (ClassNotFoundException e) { + log.debug( + "Can't find class '{}': Axaway integration failed. ", SERVERTRANSACTION_CLASSNAME, e); + } + return null; + } + + private static MethodHandle initNoArgServerTransactionMethodHandle(String name, Class rtype) { + try { + return lookup.findVirtual(classServerTransaction, name, methodType(rtype)); + } catch (NoSuchMethodException | IllegalAccessException e) { + log.debug("Can't find method handler '{}' ", name, e); + } + return null; + } + + private static MethodHandle initGetURI() { + Method m = null; + try { + m = classServerTransaction.getDeclaredMethod("getURI"); // private method + m.setAccessible(true); + return lookup.unreflect(m); + } catch (Throwable e) { + log.debug("Can't unreflect method '{}': ", m, e); + } + return null; + } + + private static MethodHandle initStateFieldGetter(String fieldName) { + Field field = null; + MethodHandle mh = null; + try { + field = classState.getDeclaredField(fieldName); + field.setAccessible(true); + mh = lookup.unreflectGetter(field); + log.debug( + "Initialized field '{}' of class '{}' unreflected to {}", fieldName, classState, mh); + } catch (NoSuchFieldException | IllegalAccessException e) { + log.debug( + "Can't find and unreflect declared field '{}' with name '{}' for class '{}' to mh: '{}'", + field, + fieldName, + classState, + mh, + e); + } + return mh; + } + + @Override + protected String[] instrumentationNames() { + return new String[] {"axway-http"}; + } + + @Override + protected String component() { + return "axway-http"; + } + + @Override + protected String method(final Object serverTransaction) { + try { + return (String) getMethod_mh.invoke(serverTransaction); + } catch (Throwable throwable) { + log.debug( + "Can't invoke invoke '{}' on instance '{}' of class '{}'", + getMethod_mh, + serverTransaction, + serverTransaction.getClass(), + throwable); + } + return "UNKNOWN"; + } + + @Override + protected URIDataAdapter url(final Object serverTransaction) { + try { + return new DefaultURIDataAdapter((URI) getURI_mh.invoke(serverTransaction)); + } catch (Throwable e) { + log.debug("Can't find invoke '{}}' on '{}': ", getURI_mh, serverTransaction, e); + } + return null; + } + + @Override + protected String peerHostIP(Object serverTransaction) { + return getRemoteAddr(serverTransaction).getHostString(); + } + + @Override + protected int peerPort(Object serverTransaction) { + return getRemoteAddr(serverTransaction).getPort(); + } + + /** @param serverTransaction instance of {@value #SERVERTRANSACTION_CLASSNAME} */ + @Override + protected int status(final Object serverTransaction) { + // TODO will be done manually + return 0; + } + + /** @param stateInstance type com.vordel.circuit.net.State */ + public AgentSpan onTransaction(AgentSpan span, Object stateInstance) { + if (span != null) { + setStringTagFromStateField(span, Tags.PEER_HOSTNAME, stateInstance, hostField_mh); + setStringTagFromStateField(span, Tags.PEER_PORT, stateInstance, portField_mh); + setStringTagFromStateField(span, Tags.HTTP_METHOD, stateInstance, methodField_mh); + setURLTagFromUriStateField(span, stateInstance); + } + return span; + } + + /** + * class hierarchy in package com.vordel.dwe.http : + * + *

+   * public class Transaction implements IMetricsTransaction {
+   *    public native InetSocketAddress getLocalAddr();
+   *    public native InetSocketAddress getRemoteAddr();
+   * }
+   * public abstract class HTTPTransaction extends Transaction {
+   *
+   * }
+   * public class ServerTransaction extends HTTPTransaction {
+   *
+   * }
+   * 
+ * + * @param obj instance of {@value #SERVERTRANSACTION_CLASSNAME} + * @return result of {@value #SERVERTRANSACTION_CLASSNAME}::getRemoteAddr() + */ + private static InetSocketAddress getRemoteAddr(Object obj) { + try { + return (InetSocketAddress) getRemoteAddr_mh.invoke(obj); + } catch (Throwable throwable) { + log.debug("Can't invoke '{}' on instance '{}': ", getRemoteAddr_mh, obj, throwable); + } + return new InetSocketAddress(0); + } + + private static void setStringTagFromStateField( + AgentSpan span, String tag, Object stateInstance, MethodHandle mh) { + String v = ""; + try { + v = (String) mh.invoke(stateInstance); + } catch (Throwable e) { + log.debug( + "Can't invoke '{}' on instance '{}'; ; Tag '{}' not set.", mh, stateInstance, tag, e); + } + span.setTag(tag, v); + } + + private static void setURLTagFromUriStateField(AgentSpan span, Object stateInstance) { + try { + span.setTag(Tags.HTTP_URL, uriField_mh.invoke(stateInstance).toString()); + } catch (Throwable e) { + log.debug( + "Can't invoke '{}' on instance '{}'; Tag '{}' not set.", + uriField_mh, + stateInstance, + Tags.HTTP_URL, + e); + } + } +} diff --git a/dd-java-agent/instrumentation/axway-api/src/main/java/datadog/trace/instrumentation/axway/AxwayHTTPPluginInstrumentation.java b/dd-java-agent/instrumentation/axway-api/src/main/java/datadog/trace/instrumentation/axway/AxwayHTTPPluginInstrumentation.java new file mode 100644 index 00000000000..ec98e893e4c --- /dev/null +++ b/dd-java-agent/instrumentation/axway-api/src/main/java/datadog/trace/instrumentation/axway/AxwayHTTPPluginInstrumentation.java @@ -0,0 +1,59 @@ +package datadog.trace.instrumentation.axway; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.namedOneOf; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import java.util.HashMap; +import java.util.Map; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public final class AxwayHTTPPluginInstrumentation extends Instrumenter.Tracing { + + public AxwayHTTPPluginInstrumentation() { + super("axway-api"); + } + + @Override + public Map contextStore() { + return singletonMap("com.vordel.dwe.http.ServerTransaction", int.class.getName()); + } + + @Override + public ElementMatcher typeMatcher() { + return namedOneOf( + "com.vordel.dwe.http.HTTPPlugin", + "com.vordel.dwe.http.ServerTransaction", + "com.vordel.circuit.net.State"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".StateAdvice", + packageName + ".AxwayHTTPPluginDecorator", + packageName + ".HTTPPluginAdvice", + packageName + ".ServerTransactionAdvice", + }; + } + + @Override + public Map, String> transformers() { + final Map, String> transformers = new HashMap<>(); + transformers.put( + isMethod().and(isPublic()).and(named("invokeDispose")), packageName + ".HTTPPluginAdvice"); + transformers.put( + isMethod().and(isPublic()).and(named("tryTransaction")), packageName + ".StateAdvice"); + transformers.put( + isMethod().and(isPublic()).and(named("sendResponse")), + packageName + ".ServerTransactionAdvice"); + return transformers; + } +} diff --git a/dd-java-agent/instrumentation/axway-api/src/main/java/datadog/trace/instrumentation/axway/HTTPPluginAdvice.java b/dd-java-agent/instrumentation/axway-api/src/main/java/datadog/trace/instrumentation/axway/HTTPPluginAdvice.java new file mode 100644 index 00000000000..d5a29b366bf --- /dev/null +++ b/dd-java-agent/instrumentation/axway-api/src/main/java/datadog/trace/instrumentation/axway/HTTPPluginAdvice.java @@ -0,0 +1,57 @@ +package datadog.trace.instrumentation.axway; + +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.axway.AxwayHTTPPluginDecorator.AXWAY_REQUEST; +import static datadog.trace.instrumentation.axway.AxwayHTTPPluginDecorator.DECORATE; +import static datadog.trace.instrumentation.axway.AxwayHTTPPluginDecorator.SERVER_TRANSACTION_CLASS; + +import datadog.trace.bootstrap.InstrumentationContext; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import net.bytebuddy.asm.Advice; + +public class HTTPPluginAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope onEnter(@Advice.Argument(value = 2) final Object serverTransaction) { + final AgentSpan span = startSpan(AXWAY_REQUEST); + final AgentScope scope = activateSpan(span); + span.setMeasured(true); + DECORATE.afterStart(span); + // serverTransaction is like request + connection in one object: + DECORATE.onRequest(span, serverTransaction, serverTransaction, null); + return scope; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Enter final AgentScope scope, + @Advice.Argument(value = 2) final Object serverTransaction, + @Advice.Thrown final Throwable throwable) { + if (scope == null) { + return; + } + final AgentSpan span = scope.span(); + try { + if (null != serverTransaction) { + // manual DECORATE.onResponse(span, serverTransaction): + // TODO: It doesn't work. Rewriting of InstrumentationContext.get fails here, because both + // arguments should be + // class-literals (not runtime Class object) to make FieldBackedContextRequestRewriter + // work. + int respCode = + InstrumentationContext.get(SERVER_TRANSACTION_CLASS, int.class).get(serverTransaction); + span.setTag(Tags.HTTP_STATUS, respCode); + } + if (throwable != null) { + DECORATE.onError(span, throwable); + } + DECORATE.beforeFinish(span); + } finally { + scope.close(); + span.finish(); + } + } +} diff --git a/dd-java-agent/instrumentation/axway-api/src/main/java/datadog/trace/instrumentation/axway/ServerTransactionAdvice.java b/dd-java-agent/instrumentation/axway-api/src/main/java/datadog/trace/instrumentation/axway/ServerTransactionAdvice.java new file mode 100644 index 00000000000..54a5643ecc0 --- /dev/null +++ b/dd-java-agent/instrumentation/axway-api/src/main/java/datadog/trace/instrumentation/axway/ServerTransactionAdvice.java @@ -0,0 +1,17 @@ +package datadog.trace.instrumentation.axway; + +import static datadog.trace.instrumentation.axway.AxwayHTTPPluginDecorator.SERVER_TRANSACTION_CLASS; + +import datadog.trace.bootstrap.InstrumentationContext; +import net.bytebuddy.asm.Advice; + +public class ServerTransactionAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onEnter( + @Advice.This Object thiz, @Advice.Argument(value = 0) final int responseCode) { + if (null != SERVER_TRANSACTION_CLASS) { + InstrumentationContext.get(SERVER_TRANSACTION_CLASS, int.class).put(thiz, responseCode); + } + } +} diff --git a/dd-java-agent/instrumentation/axway-api/src/main/java/datadog/trace/instrumentation/axway/StateAdvice.java b/dd-java-agent/instrumentation/axway-api/src/main/java/datadog/trace/instrumentation/axway/StateAdvice.java new file mode 100644 index 00000000000..c9cc01ba869 --- /dev/null +++ b/dd-java-agent/instrumentation/axway-api/src/main/java/datadog/trace/instrumentation/axway/StateAdvice.java @@ -0,0 +1,47 @@ +package datadog.trace.instrumentation.axway; + +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.axway.AxwayHTTPPluginDecorator.AXWAY_TRY_TRANSACTION; +import static datadog.trace.instrumentation.axway.AxwayHTTPPluginDecorator.DECORATE; + +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import net.bytebuddy.asm.Advice; + +/** + * Axway apigateway server gathers responses from 1 or more services, aggregates them and sends + * response(s) to client(s). Apigateway is just reverse proxy. com.vordel.circuit.net.State class + * represents connection(s) and "state" of communication to one or more of the services from which + * axway apigateway needs to get reply to prepare aggregates response to customer. This + * instrumentation intends to see to which services apigateway goes to prepare it response. + */ +public class StateAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope onEnter(@Advice.This final Object stateInstance) { + final AgentSpan span = startSpan(AXWAY_TRY_TRANSACTION); + final AgentScope scope = activateSpan(span); + span.setMeasured(true); + DECORATE.onTransaction(span, stateInstance); + DECORATE.afterStart(span); + return scope; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) { + if (scope == null) { + return; + } + final AgentSpan span = scope.span(); + try { + if (throwable != null) { + DECORATE.onError(span, throwable); + } + DECORATE.beforeFinish(span); + } finally { + scope.close(); + span.finish(); + } + } +} diff --git a/settings.gradle b/settings.gradle index 20d754ab1e3..fff89bd86d7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -88,6 +88,7 @@ include ':dd-java-agent:instrumentation:apache-httpclient-4' include ':dd-java-agent:instrumentation:aws-java-sdk-1.11.0' include ':dd-java-agent:instrumentation:aws-java-sdk-2.2' include ':dd-java-agent:instrumentation:axis-2' +include ':dd-java-agent:instrumentation:axway-api' include ':dd-java-agent:instrumentation:cdi-1.2' include ':dd-java-agent:instrumentation:classloading' include ':dd-java-agent:instrumentation:classloading:jboss-testing'