From 4c6b40fc2e751404a4b3e30e71d5815697d7aaf4 Mon Sep 17 00:00:00 2001 From: Sander Hautvast Date: Thu, 31 May 2018 09:01:31 +0200 Subject: [PATCH] call stack --- agent.iml | 48 +- pom.xml | 18 +- src/main/java/perfix/Agent.java | 7 +- src/main/java/perfix/MethodInvocation.java | 21 +- src/main/java/perfix/MutableBoolean.java | 6 +- src/main/java/perfix/Registry.java | 47 +- .../perfix/instrument/ClassInstrumentor.java | 105 ++++ .../java/perfix/instrument/Instrumentor.java | 65 +++ .../JdbcInstrumentor.java} | 458 +++++++----------- .../instrument/ServletInstrumentor.java | 41 ++ src/main/java/perfix/server/HTTPServer.java | 24 +- .../server/json/SynthSerializerFactory.java | 91 ++-- src/main/resources/index.html | 13 +- src/main/resources/perfix.js | 1 + src/main/resources/tree.js | 141 ++++++ .../java/testperfix/cmdline/App.java} | 4 +- src/testapp/java/testperfix/web/WebApp.java | 34 ++ 17 files changed, 722 insertions(+), 402 deletions(-) create mode 100644 src/main/java/perfix/instrument/ClassInstrumentor.java create mode 100644 src/main/java/perfix/instrument/Instrumentor.java rename src/main/java/perfix/{ClassInstrumentor.java => instrument/JdbcInstrumentor.java} (57%) create mode 100644 src/main/java/perfix/instrument/ServletInstrumentor.java create mode 100644 src/main/resources/tree.js rename src/{deploy/java/testperfix/Main.java => testapp/java/testperfix/cmdline/App.java} (97%) create mode 100644 src/testapp/java/testperfix/web/WebApp.java diff --git a/agent.iml b/agent.iml index c297426..62ccc36 100644 --- a/agent.iml +++ b/agent.iml @@ -1,12 +1,26 @@ + + + + + + + + + + + + + + - + @@ -15,9 +29,39 @@ + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index ebc822a..8d8eee2 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,13 @@ javassist 3.21.0-GA + + + org.nanohttpd + nanohttpd + 2.3.1 + + junit junit @@ -32,9 +39,10 @@ test - org.nanohttpd - nanohttpd - 2.3.1 + org.springframework.boot + spring-boot-starter-web + 1.5.3.RELEASE + test @@ -44,9 +52,9 @@ - src/deploy/java + src/testapp/java - + maven-jar-plugin 3.1.0 diff --git a/src/main/java/perfix/Agent.java b/src/main/java/perfix/Agent.java index a694313..55a20ca 100644 --- a/src/main/java/perfix/Agent.java +++ b/src/main/java/perfix/Agent.java @@ -1,5 +1,6 @@ package perfix; +import perfix.instrument.Instrumentor; import perfix.server.HTTPServer; import java.lang.instrument.Instrumentation; @@ -16,15 +17,15 @@ public class Agent { private static final String INCLUDES_PROPERTY = "perfix.includes"; private static final String DEFAULT_PORT = "2048"; - private static final String MESSAGE = " --- Perfix agent active --- "; + private static final String MESSAGE = " --- Perfix agent active"; - public static void premain(String agentArgs, Instrumentation inst) { + public static void premain(String agentArgs, Instrumentation instrumentation) { System.out.println(MESSAGE); int port = Integer.parseInt(System.getProperty(PORT_PROPERTY, DEFAULT_PORT)); - new ClassInstrumentor(determineIncludes()).instrumentCode(inst); + Instrumentor.create(determineIncludes()).instrumentCode(instrumentation); new HTTPServer(port).start(); } diff --git a/src/main/java/perfix/MethodInvocation.java b/src/main/java/perfix/MethodInvocation.java index a84976c..7b519af 100644 --- a/src/main/java/perfix/MethodInvocation.java +++ b/src/main/java/perfix/MethodInvocation.java @@ -1,11 +1,14 @@ package perfix; +/** + * contains start and stop time for method/query/servlet + */ public class MethodInvocation { private final long t0; private final String name; - private long t1; + long t1; - private MethodInvocation(String name) { + MethodInvocation(String name) { t0 = System.nanoTime(); if (name != null) { this.name = name; @@ -14,19 +17,6 @@ public class MethodInvocation { } } - public static MethodInvocation start(String name) { - return new MethodInvocation(name); - } - - public static void stop(MethodInvocation methodInvocation) { - methodInvocation.stop(); - } - - public void stop() { - t1 = System.nanoTime(); - Registry.add(this); - } - String getName() { return name; } @@ -34,4 +24,5 @@ public class MethodInvocation { long getDuration() { return t1 - t0; } + } diff --git a/src/main/java/perfix/MutableBoolean.java b/src/main/java/perfix/MutableBoolean.java index 1d9b7f7..d9e5671 100644 --- a/src/main/java/perfix/MutableBoolean.java +++ b/src/main/java/perfix/MutableBoolean.java @@ -3,15 +3,15 @@ package perfix; public class MutableBoolean { private boolean value; - MutableBoolean(boolean value) { + public MutableBoolean(boolean value) { this.value = value; } - void set(boolean value) { + public void set(boolean value) { this.value = value; } - boolean get() { + public boolean get() { return value; } diff --git a/src/main/java/perfix/Registry.java b/src/main/java/perfix/Registry.java index 54fafda..df58cab 100644 --- a/src/main/java/perfix/Registry.java +++ b/src/main/java/perfix/Registry.java @@ -1,6 +1,5 @@ package perfix; -import java.io.PrintStream; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListMap; @@ -9,31 +8,26 @@ import java.util.concurrent.atomic.LongAdder; public class Registry { private static final Map> methods = new ConcurrentHashMap<>(); - private static final double NANO_2_MILLI = 1000000D; - private static final String HEADER1 = "Invoked methods, by duration desc:"; - private static final String HEADER2 = "MethodInvocation name;#Invocations;Total duration;Average Duration"; - private static final String FOOTER = "----------------------------------------"; + private static final Map> callstack = new ConcurrentHashMap<>(); + private static final ThreadLocal currentMethod = new ThreadLocal<>(); - static void add(MethodInvocation methodInvocation) { + @SuppressWarnings("unused") + public static MethodInvocation start(String name) { + MethodInvocation methodInvocation = new MethodInvocation(name); + String parent = currentMethod.get(); + if (parent != null) { + callstack.computeIfAbsent(parent, k -> new HashSet<>()).add(methodInvocation.getName()); + } + currentMethod.set(methodInvocation.getName()); + return methodInvocation; + } + + + @SuppressWarnings("unused") + public static void stop(MethodInvocation methodInvocation) { + methodInvocation.t1 = System.nanoTime(); methods.computeIfAbsent(methodInvocation.getName(), key -> new ArrayList<>()).add(methodInvocation); } - - public static void report(PrintStream out) { - out.println(HEADER1); - out.println(HEADER2); - sortedMethodsByDuration().entrySet().stream() - .map(entry -> createReportLine(entry.getValue())) - .forEach(out::println); - out.println(FOOTER); - out.flush(); - } - - private static String createReportLine(Report report) { - return report.getName() + ";" - + report.getInvocations() + ";" - + (long) (report.getTotalDuration() / NANO_2_MILLI) + ";" - + (long) (report.getAverage() / NANO_2_MILLI); - } public static SortedMap sortedMethodsByDuration() { SortedMap sortedByTotal = new ConcurrentSkipListMap<>(Comparator.reverseOrder()); @@ -46,6 +40,11 @@ public class Registry { }); return sortedByTotal; } +//work in progress +// public static Map> getCallStack() { +// callstack.forEach((name,children)->{ +// +// }); +// } - } diff --git a/src/main/java/perfix/instrument/ClassInstrumentor.java b/src/main/java/perfix/instrument/ClassInstrumentor.java new file mode 100644 index 0000000..298fa9b --- /dev/null +++ b/src/main/java/perfix/instrument/ClassInstrumentor.java @@ -0,0 +1,105 @@ +package perfix.instrument; + +import javassist.CannotCompileException; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.NotFoundException; +import perfix.MutableBoolean; + +import java.io.IOException; +import java.lang.instrument.Instrumentation; +import java.util.List; + +import static java.util.Arrays.stream; + +public class ClassInstrumentor extends Instrumentor { + + private static final String JAVA_INNERCLASS_SEPARATOR = "$"; + + JdbcInstrumentor jdbcInstrumentor; + ServletInstrumentor servletInstrumentor; + + ClassInstrumentor(List includes, ClassPool classPool) { + super(includes, classPool); + try { + perfixMethodInvocationClass = getCtClass(PERFIX_METHODINVOCATION_CLASS); + stringClass = classpool.get(JAVA_STRING); + + } catch (NotFoundException e) { + //suppress TODO implement trace + } + } + + public void instrumentCode(Instrumentation inst) { + inst.addTransformer((classLoader, resource, aClass, protectionDomain, uninstrumentedByteCode) + -> instrumentCodeForClass(includes, resource, uninstrumentedByteCode)); + } + + private byte[] instrumentCodeForClass(List includes, String resource, byte[] uninstrumentedByteCode) { + if (!isInnerClass(resource)) { + try { + CtClass ctClass = getCtClassForResource(resource); + + if (servletInstrumentor.isServletImpl(resource)){ + return servletInstrumentor.instrumentServlet(ctClass, uninstrumentedByteCode); + } + + if (jdbcInstrumentor.isJdbcStatementImpl(resource, ctClass)) { + return jdbcInstrumentor.instrumentJdbcStatement(ctClass, uninstrumentedByteCode); + } + + if (jdbcInstrumentor.isJdbcConnectionImpl(resource, ctClass)) { + return jdbcInstrumentor.instrumentJdbcConnection(ctClass, uninstrumentedByteCode); + } + + if (jdbcInstrumentor.isJdbcPreparedStatement(resource)) { + return jdbcInstrumentor.instrumentJdbcPreparedStatement(ctClass, uninstrumentedByteCode); + } + if (jdbcInstrumentor.isJdbcPreparedStatementImpl(resource, ctClass)) { + return jdbcInstrumentor.instrumentJdbcPreparedStatementImpl(ctClass, uninstrumentedByteCode); + } + if (shouldInclude(resource, includes)) { + return instrumentMethods(ctClass, uninstrumentedByteCode); + } + } catch (Exception ex) { + //suppress + } + } + return uninstrumentedByteCode; + } + + /* for regular classes that require instrumentation instrument all methods to record duration*/ + private byte[] instrumentMethods(CtClass classToInstrument, byte[] uninstrumentedByteCode) { + if (!classToInstrument.isInterface()) { + stream(classToInstrument.getDeclaredMethods()).forEach(this::instrumentMethod); + + try { + return bytecode(classToInstrument); + } catch (IOException | CannotCompileException e) { + //suppress + } + } + return uninstrumentedByteCode; + + } + + private CtClass getCtClassForResource(String resource) throws NotFoundException { + return getCtClass(resource.replaceAll("/", ".")); + } + + private boolean shouldInclude(String resource, List excludes) { + MutableBoolean included = new MutableBoolean(false); + excludes.forEach(include -> { + if (resource.startsWith(include)) { + included.set(true); + } + }); + return included.get(); + } + + private boolean isInnerClass(String resource) { + return resource.contains(JAVA_INNERCLASS_SEPARATOR); + } + + +} diff --git a/src/main/java/perfix/instrument/Instrumentor.java b/src/main/java/perfix/instrument/Instrumentor.java new file mode 100644 index 0000000..981c98d --- /dev/null +++ b/src/main/java/perfix/instrument/Instrumentor.java @@ -0,0 +1,65 @@ +package perfix.instrument; + +import javassist.*; + +import java.io.IOException; +import java.lang.instrument.Instrumentation; +import java.util.List; + +public abstract class Instrumentor { + static final String JAVA_STRING = "java.lang.String"; + static final String PERFIX_METHODINVOCATION_CLASS = "perfix.MethodInvocation"; + static final String JAVASSIST_FIRST_ARGUMENT_NAME = "$1"; + static final String JAVASSIST_RETURNVALUE = "$_"; + + final ClassPool classpool; + final List includes; + CtClass perfixMethodInvocationClass; + CtClass stringClass; + + Instrumentor(List includes, ClassPool classPool) { + this.includes = includes; + this.classpool = classPool; + try { + perfixMethodInvocationClass = getCtClass(PERFIX_METHODINVOCATION_CLASS); + stringClass = classpool.get(JAVA_STRING); + + } catch (NotFoundException e) { + //suppress TODO implement trace + } + } + + public static Instrumentor create(List includes) { + ClassPool classPool = ClassPool.getDefault(); + ClassInstrumentor classInstrumentor = new ClassInstrumentor(includes, classPool); + classInstrumentor.jdbcInstrumentor = new JdbcInstrumentor(includes, classPool); + classInstrumentor.servletInstrumentor = new ServletInstrumentor(includes, classPool); + return classInstrumentor; + } + + public void instrumentCode(Instrumentation inst){} + + void instrumentMethod(CtMethod methodToinstrument) { + instrumentMethod(methodToinstrument, "\"" + methodToinstrument.getLongName() + "\""); + } + + /* record times at beginning and end of method body*/ + void instrumentMethod(CtMethod methodToinstrument, String metricName) { + try { + methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass); + methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.start(" + metricName + ");"); + methodToinstrument.insertAfter("perfix.Registry.stop(_perfixmethod);"); + } catch (CannotCompileException e) { + throw new RuntimeException(e); + } + } + + byte[] bytecode(CtClass classToInstrument) throws IOException, CannotCompileException { + classToInstrument.detach(); + return classToInstrument.toBytecode(); + } + + CtClass getCtClass(String classname) throws NotFoundException { + return classpool.get(classname); + } +} diff --git a/src/main/java/perfix/ClassInstrumentor.java b/src/main/java/perfix/instrument/JdbcInstrumentor.java similarity index 57% rename from src/main/java/perfix/ClassInstrumentor.java rename to src/main/java/perfix/instrument/JdbcInstrumentor.java index 0c559e7..a85e4dd 100644 --- a/src/main/java/perfix/ClassInstrumentor.java +++ b/src/main/java/perfix/instrument/JdbcInstrumentor.java @@ -1,284 +1,174 @@ -package perfix; - -import javassist.*; - -import java.io.IOException; -import java.lang.instrument.Instrumentation; -import java.util.List; - -import static java.util.Arrays.stream; - -class ClassInstrumentor { - - private static final String JAVA_INNERCLASS_SEPARATOR = "$"; - private static final String JAVA_STRING = "java.lang.String"; - private static final String JAVASQL_PACKAGE = "java/sql"; - private static final String JAVASQL_STATEMENT_INTERFACE = "java.sql.Statement"; - private static final String JAVASQL_EXECUTE_METHOD = "execute"; - private static final String JAVASQL_EXECUTEQUERY_METHOD = "executeQuery"; - private static final String JAVASQL_EXECUTEUPDATE_METHOD = "executeUpdate"; - private static final String JAVASQL_PREPAREDSTATEMENT_INTERFACE = "java.sql.PreparedStatement"; - private static final String JAVASQL_CONNECTION_INTERFACE = "java.sql.Connection"; - private static final String JAVASQL_PREPARED_STATEMENT_CLASSNAME = "java.sql.PreparedStatement"; - private static final String JAVASQL_PREPAREDSTATEMENT_RESOURCENAME = "java/sql/PreparedStatement"; - private static final String JAVASQL_PREPARESTATEMENT_METHODNAME = "prepareStatement"; - - private static final String JAVASSIST_FIRST_ARGUMENT_NAME = "$1"; - private static final String JAVASSIST_RETURNVALUE = "$_"; - - private static final String PERFIX_METHODINVOCATION_CLASS = "perfix.MethodInvocation"; - private static final String PERFIX_SQLSTATEMENT_FIELD = "_perfixSqlStatement"; - private static final String PERFIX_SETSQL_METHOD = "setSqlForPerfix"; - - private ClassPool classpool; - private List includes; - private CtClass perfixMethodInvocationClass; - private CtClass stringClass; - - ClassInstrumentor(List includes) { - this.includes = includes; - this.classpool = ClassPool.getDefault(); - try { - perfixMethodInvocationClass = getCtClass(PERFIX_METHODINVOCATION_CLASS); - stringClass = classpool.get(JAVA_STRING); - - } catch (NotFoundException e) { - //suppress TODO implement trace - } - } - - void instrumentCode(Instrumentation inst) { - inst.addTransformer((classLoader, resource, aClass, protectionDomain, uninstrumentedByteCode) - -> instrumentCodeForClass(includes, resource, uninstrumentedByteCode)); - } - - private byte[] instrumentCodeForClass(List includes, String resource, byte[] uninstrumentedByteCode) { - if (!isInnerClass(resource)) { - try { - CtClass ctClass = getCtClassForResource(resource); - - if (isJdbcStatementImpl(resource, ctClass)) { - return instrumentJdbcStatement(ctClass, uninstrumentedByteCode); - } - - if (isJdbcConnectionImpl(resource, ctClass)) { - return instrumentJdbcConnection(ctClass, uninstrumentedByteCode); - } - - if (isJdbcPreparedStatement(resource)) { - return instrumentJdbcPreparedStatement(ctClass, uninstrumentedByteCode); - } - if (isJdbcPreparedStatementImpl(resource, ctClass)) { - return instrumentJdbcPreparedStatementImpl(ctClass, uninstrumentedByteCode); - } - if (shouldInclude(resource, includes)) { - return instrumentMethods(ctClass, uninstrumentedByteCode); - } - } catch (Exception ex) { - //suppress - } - } - return uninstrumentedByteCode; - } - - /* Need to enhance interface to be able to set a statement (string) for perfix. */ - private byte[] instrumentJdbcPreparedStatement(CtClass preparedStatementInterface, byte[] uninstrumentedByteCode) { - try { - preparedStatementInterface.getDeclaredMethod(PERFIX_SETSQL_METHOD); - } catch (NotFoundException e1) { - try { - CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, PERFIX_SETSQL_METHOD, new CtClass[]{stringClass}, preparedStatementInterface); - preparedStatementInterface.addMethod(setSqlForPerfix); - - } catch (CannotCompileException e2) { - return uninstrumentedByteCode; - } - } - try { - return bytecode(preparedStatementInterface); - } catch (CannotCompileException | IOException e) { - return uninstrumentedByteCode; - } - } - - /* Prepared statement methods that actually execute sql don't have the statement in their parameters (unlike java.sql.Statement) - * instead every jdbc vendor has a specific String field in their PreparedStatement impl that contains the statement. - * - * To circumvent vendor specifics in perfix, the first argument for prepareStatement (the sql String) is intercepted here - * and injected into the PreparedStatement instance under a fixed name, whatever the implementation type */ - private byte[] instrumentJdbcConnection(CtClass classToInstrument, byte[] uninstrumentedByteCode) { - try { - stream(classToInstrument.getDeclaredMethods(JAVASQL_PREPARESTATEMENT_METHODNAME)).forEach(method -> { - try { - // The sql statement String that is the first argument for this method is injected into PreparedStatementImpl - // using a name known (only) to perfix, so that it can fetch it later in that class (instance) - // this way no JDBC implementor specific code is needed - CtClass preparedStatementInterface = classpool.get(JAVASQL_PREPARED_STATEMENT_CLASSNAME); - - try { - preparedStatementInterface.getDeclaredMethod(PERFIX_SETSQL_METHOD); - } catch (NotFoundException e1) { - CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, PERFIX_SETSQL_METHOD, new CtClass[]{stringClass}, preparedStatementInterface); - preparedStatementInterface.addMethod(setSqlForPerfix); - } - - method.insertAfter(JAVASSIST_RETURNVALUE + "." + PERFIX_SETSQL_METHOD + "(" + JAVASSIST_FIRST_ARGUMENT_NAME + ");"); //$_ is result instance, $1 is first argument - } catch (CannotCompileException | NotFoundException e) { - // suppress - e.printStackTrace(); - } - } - ); - return bytecode(classToInstrument); - } catch (NotFoundException | CannotCompileException | IOException e) { - // suppress - } - return uninstrumentedByteCode; - } - - /* */ - private byte[] instrumentJdbcPreparedStatementImpl(CtClass preparedStatementClass, byte[] uninstrumentedByteCode) { - try { - addPerfixStatementField(preparedStatementClass); - addPerfixStatementSetter(preparedStatementClass); - - // instrument execute to record query duration - stream(preparedStatementClass.getDeclaredMethods(JAVASQL_EXECUTE_METHOD)).forEach(method -> - instrumentMethod(method, PERFIX_SQLSTATEMENT_FIELD) - ); - - // instrument executeQuery to record query duration - stream(preparedStatementClass.getDeclaredMethods(JAVASQL_EXECUTEQUERY_METHOD)).forEach(method -> - instrumentMethod(method, PERFIX_SQLSTATEMENT_FIELD) - ); - - // instrument executeUpdate to record query duration - stream(preparedStatementClass.getDeclaredMethods(JAVASQL_EXECUTEUPDATE_METHOD)).forEach(method -> - instrumentMethod(method, PERFIX_SQLSTATEMENT_FIELD) - ); - - return bytecode(preparedStatementClass); - } catch (NotFoundException | CannotCompileException | IOException e) { - e.printStackTrace(); - return uninstrumentedByteCode; - } - } - - - private byte[] instrumentJdbcStatement(CtClass classToInstrument, byte[] uninstrumentedByteCode) { - try { - //instrument executeQuery to record query duration - stream(classToInstrument.getDeclaredMethods(JAVASQL_EXECUTEQUERY_METHOD)).forEach(method -> - instrumentMethod(method, JAVASSIST_FIRST_ARGUMENT_NAME) - ); - //instrument executeUpdate to record query duration - stream(classToInstrument.getDeclaredMethods(JAVASQL_EXECUTEUPDATE_METHOD)).forEach(method -> - instrumentMethod(method, JAVASSIST_FIRST_ARGUMENT_NAME) - ); - return bytecode(classToInstrument); - } catch (Exception e) { - return uninstrumentedByteCode; - } - - } - - /* for regular classes that require instrumentation instrument all methods to record duration*/ - private byte[] instrumentMethods(CtClass classToInstrument, byte[] uninstrumentedByteCode) { - if (!classToInstrument.isInterface()) { - stream(classToInstrument.getDeclaredMethods()).forEach(this::instrumentMethod); - - try { - return bytecode(classToInstrument); - } catch (IOException | CannotCompileException e) { - //suppress - } - } - return uninstrumentedByteCode; - - } - - private CtClass getCtClassForResource(String resource) throws NotFoundException { - return getCtClass(resource.replaceAll("/", ".")); - } - - private CtClass getCtClass(String classname) throws NotFoundException { - return classpool.get(classname); - } - - private void instrumentMethod(CtMethod methodToinstrument) { - instrumentMethod(methodToinstrument, "\"" + methodToinstrument.getLongName() + "\""); - } - - /* record times at beginning and end of method body*/ - private void instrumentMethod(CtMethod methodToinstrument, String metricName) { - try { - methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass); - methodToinstrument.insertBefore("_perfixmethod = perfix.MethodInvocation.start(" + metricName + ");"); - methodToinstrument.insertAfter("perfix.MethodInvocation.stop(_perfixmethod);"); - } catch (CannotCompileException e) { - throw new RuntimeException(e); - } - } - - private boolean shouldInclude(String resource, List excludes) { - MutableBoolean included = new MutableBoolean(false); - excludes.forEach(include -> { - if (resource.startsWith(include)) { - included.set(true); - } - }); - return included.get(); - } - - private boolean isInnerClass(String resource) { - return resource.contains(JAVA_INNERCLASS_SEPARATOR); - } - - private boolean isJdbcStatementImpl(String resource, CtClass ctClass) throws NotFoundException { - if (!resource.startsWith(JAVASQL_PACKAGE)) { - return stream(ctClass.getInterfaces()) - .anyMatch(i -> i.getName().equals(JAVASQL_STATEMENT_INTERFACE) && !i.getName().equals(JAVASQL_PREPARED_STATEMENT_CLASSNAME)); - } - return false; - } - - private boolean isJdbcPreparedStatement(String resource) { - return resource.equals(JAVASQL_PREPAREDSTATEMENT_RESOURCENAME); - } - - private boolean isJdbcPreparedStatementImpl(String resource, CtClass ctClass) throws NotFoundException { - if (!resource.startsWith(JAVASQL_PACKAGE)) { - return stream(ctClass.getInterfaces()) - .anyMatch(i -> i.getName().equals(JAVASQL_PREPAREDSTATEMENT_INTERFACE)); - } - return false; - } - - private boolean isJdbcConnectionImpl(String resource, CtClass ctClass) throws NotFoundException { - if (!resource.startsWith(JAVASQL_PACKAGE)) { - return stream(ctClass.getInterfaces()) - .anyMatch(i -> i.getName().equals(JAVASQL_CONNECTION_INTERFACE)); - } - return false; - } - - private byte[] bytecode(CtClass classToInstrument) throws IOException, CannotCompileException { - classToInstrument.detach(); - return classToInstrument.toBytecode(); - } - - private void addPerfixStatementField(CtClass preparedStatementClass) throws CannotCompileException { - // add a String field that will contain the statement - CtField perfixSqlField = new CtField(stringClass, PERFIX_SQLSTATEMENT_FIELD, preparedStatementClass); - perfixSqlField.setModifiers(Modifier.PRIVATE); - preparedStatementClass.addField(perfixSqlField); - } - - private void addPerfixStatementSetter(CtClass preparedStatementImplClass) throws CannotCompileException { - // add setter for the new field - CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, PERFIX_SETSQL_METHOD, new CtClass[]{stringClass}, preparedStatementImplClass); - setSqlForPerfix.setModifiers(Modifier.PUBLIC); - setSqlForPerfix.setBody(PERFIX_SQLSTATEMENT_FIELD + "=" + JAVASSIST_FIRST_ARGUMENT_NAME + ";"); - preparedStatementImplClass.addMethod(setSqlForPerfix); - } -} +package perfix.instrument; + +import javassist.*; + +import java.io.IOException; +import java.util.List; + +import static java.util.Arrays.stream; + +public class JdbcInstrumentor extends Instrumentor { + + private static final String JAVASQL_EXECUTE_METHOD = "execute"; + private static final String JAVASQL_EXECUTEQUERY_METHOD = "executeQuery"; + private static final String JAVASQL_EXECUTEUPDATE_METHOD = "executeUpdate"; + private static final String JAVASQL_PACKAGE = "java/sql"; + private static final String JAVASQL_STATEMENT_INTERFACE = "java.sql.Statement"; + private static final String JAVASQL_PREPAREDSTATEMENT_INTERFACE = "java.sql.PreparedStatement"; + private static final String JAVASQL_CONNECTION_INTERFACE = "java.sql.Connection"; + private static final String JAVASQL_PREPAREDSTATEMENT_RESOURCENAME = "java/sql/PreparedStatement"; + private static final String JAVASQL_PREPARED_STATEMENT_CLASSNAME = "java.sql.PreparedStatement"; + private static final String JAVASQL_PREPARESTATEMENT_METHODNAME = "prepareStatement"; + + private static final String PERFIX_SQLSTATEMENT_FIELD = "_perfixSqlStatement"; + private static final String PERFIX_SETSQL_METHOD = "setSqlForPerfix"; + + + JdbcInstrumentor(List includes, ClassPool classPool) { + super(includes, classPool); + } + + /* Need to enhance interface to be able to set a statement (string) for perfix. */ + byte[] instrumentJdbcPreparedStatement(CtClass preparedStatementInterface, byte[] uninstrumentedByteCode) { + try { + preparedStatementInterface.getDeclaredMethod(PERFIX_SETSQL_METHOD); + } catch (NotFoundException e1) { + try { + CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, PERFIX_SETSQL_METHOD, new CtClass[]{stringClass}, preparedStatementInterface); + preparedStatementInterface.addMethod(setSqlForPerfix); + + } catch (CannotCompileException e2) { + return uninstrumentedByteCode; + } + } + try { + return bytecode(preparedStatementInterface); + } catch (CannotCompileException | IOException e) { + return uninstrumentedByteCode; + } + } + + /* Prepared statement methods that actually execute sql don't have the statement in their parameters (unlike java.sql.Statement) + * instead every jdbc vendor has a specific String field in their PreparedStatement impl that contains the statement. + * + * To circumvent vendor specifics in perfix, the first argument for prepareStatement (the sql String) is intercepted here + * and injected into the PreparedStatement instance under a fixed name, whatever the implementation type */ + byte[] instrumentJdbcConnection(CtClass classToInstrument, byte[] uninstrumentedByteCode) { + try { + stream(classToInstrument.getDeclaredMethods(JAVASQL_PREPARESTATEMENT_METHODNAME)).forEach(method -> { + try { + // The sql statement String that is the first argument for this method is injected into PreparedStatementImpl + // using a name known (only) to perfix, so that it can fetch it later in that class (instance) + // this way no JDBC implementor specific code is needed + CtClass preparedStatementInterface = classpool.get(JAVASQL_PREPARED_STATEMENT_CLASSNAME); + + try { + preparedStatementInterface.getDeclaredMethod(PERFIX_SETSQL_METHOD); + } catch (NotFoundException e1) { + CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, PERFIX_SETSQL_METHOD, new CtClass[]{stringClass}, preparedStatementInterface); + preparedStatementInterface.addMethod(setSqlForPerfix); + } + + method.insertAfter(JAVASSIST_RETURNVALUE + "." + PERFIX_SETSQL_METHOD + "(" + JAVASSIST_FIRST_ARGUMENT_NAME + ");"); //$_ is result instance, $1 is first argument + } catch (CannotCompileException | NotFoundException e) { + // suppress + e.printStackTrace(); + } + } + ); + return bytecode(classToInstrument); + } catch (NotFoundException | CannotCompileException | IOException e) { + // suppress + } + return uninstrumentedByteCode; + } + + /* */ + byte[] instrumentJdbcPreparedStatementImpl(CtClass preparedStatementClass, byte[] uninstrumentedByteCode) { + try { + addPerfixStatementField(preparedStatementClass); + addPerfixStatementSetter(preparedStatementClass); + + // instrument execute to record query duration + stream(preparedStatementClass.getDeclaredMethods(JAVASQL_EXECUTE_METHOD)).forEach(method -> + instrumentMethod(method, PERFIX_SQLSTATEMENT_FIELD) + ); + + // instrument executeQuery to record query duration + stream(preparedStatementClass.getDeclaredMethods(JAVASQL_EXECUTEQUERY_METHOD)).forEach(method -> + instrumentMethod(method, PERFIX_SQLSTATEMENT_FIELD) + ); + + // instrument executeUpdate to record query duration + stream(preparedStatementClass.getDeclaredMethods(JAVASQL_EXECUTEUPDATE_METHOD)).forEach(method -> + instrumentMethod(method, PERFIX_SQLSTATEMENT_FIELD) + ); + + return bytecode(preparedStatementClass); + } catch (NotFoundException | CannotCompileException | IOException e) { + e.printStackTrace(); + return uninstrumentedByteCode; + } + } + + + byte[] instrumentJdbcStatement(CtClass classToInstrument, byte[] uninstrumentedByteCode) { + try { + //instrument executeQuery to record query duration + stream(classToInstrument.getDeclaredMethods(JAVASQL_EXECUTEQUERY_METHOD)).forEach(method -> + instrumentMethod(method, JAVASSIST_FIRST_ARGUMENT_NAME) + ); + //instrument executeUpdate to record query duration + stream(classToInstrument.getDeclaredMethods(JAVASQL_EXECUTEUPDATE_METHOD)).forEach(method -> + instrumentMethod(method, JAVASSIST_FIRST_ARGUMENT_NAME) + ); + return bytecode(classToInstrument); + } catch (Exception e) { + return uninstrumentedByteCode; + } + + } + + private void addPerfixStatementField(CtClass preparedStatementClass) throws CannotCompileException { + // add a String field that will contain the statement + CtField perfixSqlField = new CtField(stringClass, PERFIX_SQLSTATEMENT_FIELD, preparedStatementClass); + perfixSqlField.setModifiers(Modifier.PRIVATE); + preparedStatementClass.addField(perfixSqlField); + } + + private void addPerfixStatementSetter(CtClass preparedStatementImplClass) throws CannotCompileException { + // add setter for the new field + CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, PERFIX_SETSQL_METHOD, new CtClass[]{stringClass}, preparedStatementImplClass); + setSqlForPerfix.setModifiers(Modifier.PUBLIC); + setSqlForPerfix.setBody(PERFIX_SQLSTATEMENT_FIELD + "=" + JAVASSIST_FIRST_ARGUMENT_NAME + ";"); + preparedStatementImplClass.addMethod(setSqlForPerfix); + } + + boolean isJdbcStatementImpl(String resource, CtClass ctClass) throws NotFoundException { + if (!resource.startsWith(JAVASQL_PACKAGE)) { + return stream(ctClass.getInterfaces()) + .anyMatch(i -> i.getName().equals(JAVASQL_STATEMENT_INTERFACE) && !i.getName().equals(JAVASQL_PREPARED_STATEMENT_CLASSNAME)); + } + return false; + } + + boolean isJdbcPreparedStatement(String resource) { + return resource.equals(JAVASQL_PREPAREDSTATEMENT_RESOURCENAME); + } + + boolean isJdbcPreparedStatementImpl(String resource, CtClass ctClass) throws NotFoundException { + if (!resource.startsWith(JAVASQL_PACKAGE)) { + return stream(ctClass.getInterfaces()) + .anyMatch(i -> i.getName().equals(JAVASQL_PREPAREDSTATEMENT_INTERFACE)); + } + return false; + } + + boolean isJdbcConnectionImpl(String resource, CtClass ctClass) throws NotFoundException { + if (!resource.startsWith(JAVASQL_PACKAGE)) { + return stream(ctClass.getInterfaces()) + .anyMatch(i -> i.getName().equals(JAVASQL_CONNECTION_INTERFACE)); + } + return false; + } +} diff --git a/src/main/java/perfix/instrument/ServletInstrumentor.java b/src/main/java/perfix/instrument/ServletInstrumentor.java new file mode 100644 index 0000000..35a8f98 --- /dev/null +++ b/src/main/java/perfix/instrument/ServletInstrumentor.java @@ -0,0 +1,41 @@ +package perfix.instrument; + +import javassist.CannotCompileException; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.NotFoundException; + +import java.util.List; + +import static java.util.Arrays.stream; + +public class ServletInstrumentor extends Instrumentor { + + private static final String JAVA_SERVLET_SERVICE_METHOD = "service"; + private static final String JAVA_SERVLET_CLASS = "javax/servlet/http/HttpServlet"; + + ServletInstrumentor(List includes, ClassPool classPool) { + super(includes, classPool); + } + + public boolean isServletImpl(String resource) { + return resource.equals(JAVA_SERVLET_CLASS); + } + + public byte[] instrumentServlet(CtClass classToInstrument, byte[] uninstrumentedByteCode) { + try { + stream(classToInstrument.getDeclaredMethods(JAVA_SERVLET_SERVICE_METHOD)).forEach(methodToInstrument -> { + try { + methodToInstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass); + methodToInstrument.insertBefore("_perfixmethod = perfix.Registry.start($1.getRequestURI());"); + methodToInstrument.insertAfter("perfix.Registry.stop(_perfixmethod);"); + } catch (CannotCompileException e) { + + } + }); + return bytecode(classToInstrument); + } catch (Exception e) { + return uninstrumentedByteCode; + } + } +} diff --git a/src/main/java/perfix/server/HTTPServer.java b/src/main/java/perfix/server/HTTPServer.java index 2828880..992c791 100644 --- a/src/main/java/perfix/server/HTTPServer.java +++ b/src/main/java/perfix/server/HTTPServer.java @@ -19,19 +19,22 @@ public class HTTPServer extends NanoHTTPD { try { start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); - System.out.println("\nHttpServer running! Point your browser to http://localhost:2048/ \n"); + System.out.println(" --- Perfix http server running. Point your browser to http://localhost:" + getListeningPort() + "/"); } catch (IOException ioe) { - System.err.println("Couldn't start server:\n" + ioe); + System.err.println(" --- Couldn't start Perfix http server:\n" + ioe); } } @Override public Response serve(IHTTPSession session) { String uri = session.getUri(); - if (uri.equals("/report")) { - return perfixMetrics(); - } else { - return serveStaticContent(uri); + switch (uri) { + case "/report": + return perfixMetrics(); + case "/callstack": + return perfixCallstack(); + default: + return serveStaticContent(uri); } } @@ -59,6 +62,15 @@ public class HTTPServer extends NanoHTTPD { } } + private Response perfixCallstack() { + try { + return newFixedLengthResponse(Response.Status.OK, "application/json", Serializer.toJSONString(Registry.getCallStack())); + } catch (Exception e) { + e.printStackTrace(); + return newFixedLengthResponse(e.toString()); + } + } + private String readFile(InputStream stream) throws IOException { int nbytes = stream.available(); byte[] bytes = new byte[nbytes]; diff --git a/src/main/java/perfix/server/json/SynthSerializerFactory.java b/src/main/java/perfix/server/json/SynthSerializerFactory.java index 2b324e3..d55462b 100644 --- a/src/main/java/perfix/server/json/SynthSerializerFactory.java +++ b/src/main/java/perfix/server/json/SynthSerializerFactory.java @@ -1,21 +1,10 @@ package perfix.server.json; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import javassist.*; -import javassist.CannotCompileException; -import javassist.ClassPool; -import javassist.CtClass; -import javassist.CtField; -import javassist.CtMethod; -import javassist.CtNewMethod; -import javassist.Modifier; -import javassist.NotFoundException; +import java.util.*; + +import static java.util.Arrays.asList; public class SynthSerializerFactory implements SerializerFactory { private static final String STRING = "java.lang.String"; @@ -28,25 +17,29 @@ public class SynthSerializerFactory implements SerializerFactory { private static final String SHORT = "java.lang.Short"; private static final String INTEGER = "java.lang.Integer"; - private final static Set wrappersAndString = new HashSet(Arrays.asList(BOOLEAN, CHARACTER, BYTE, DOUBLE, FLOAT, LONG, SHORT, INTEGER, + private final static Set wrappersAndString = new HashSet(asList(BOOLEAN, CHARACTER, BYTE, DOUBLE, FLOAT, LONG, SHORT, INTEGER, STRING)); private static final String COLLECTION = "java.util.Collection"; private static final String LIST = "java.util.List"; private static final String SET = "java.util.Set"; - private static final String MAP = "java.util.Map"; + private static final List mapInterfaces = asList("java.util.Map", "java.util.concurrent.ConcurrentHashMap"); private static final Map> serializers = new HashMap<>(); private static final String ROOT_PACKAGE = "serializer."; private final ClassPool pool = ClassPool.getDefault(); - private CtClass serializerBase; private final Map primitiveWrappers = new HashMap(); + private CtClass serializerBase; - public SynthSerializerFactory() { + SynthSerializerFactory() { init(); } + private static boolean isPrimitiveOrWrapperOrString(CtClass beanClass) { + return beanClass.isPrimitive() || wrappersAndString.contains(beanClass.getName()); + } + void init() { try { serializerBase = pool.get(JSONSerializer.class.getName()); @@ -68,9 +61,7 @@ public class SynthSerializerFactory implements SerializerFactory { try { CtClass beanClass = pool.get(beanjavaClass.getName()); - JSONSerializer jsonSerializer = createSerializer(beanClass); - - return jsonSerializer; + return createSerializer(beanClass); } catch (NotFoundException e) { throw new SerializerCreationException(e); } @@ -104,7 +95,8 @@ public class SynthSerializerFactory implements SerializerFactory { * create method source, compile it and add it to the class under construction */ private void addToJsonStringMethod(CtClass beanClass, CtClass serializerClass) throws NotFoundException, CannotCompileException { - serializerClass.addMethod(CtNewMethod.make(createToJSONStringMethodSource(beanClass), serializerClass)); + String body = createToJSONStringMethodSource(beanClass); + serializerClass.addMethod(CtNewMethod.make(body, serializerClass)); } /* @@ -133,9 +125,9 @@ public class SynthSerializerFactory implements SerializerFactory { /* * Any Collection is converted to an array, after which code is generated to handle the single elements. - * + * * A subserializer is created for every single element, but most of the time it will be the same cached instance. - * + * * The generated code fills a StringBuilder. The values are generated by the subserializers */ private String handleArray(CtClass beanClass) { @@ -192,7 +184,7 @@ public class SynthSerializerFactory implements SerializerFactory { /* * custom root package is prepended to avoid the java.lang class in which it's illegal to create new classes - * + * * Array marks ( '[]' ) are replaced by the 'Array', Otherwise the SerializerClassName would be syntactically incorrect */ public String createSerializerName(CtClass beanClass) { @@ -204,7 +196,7 @@ public class SynthSerializerFactory implements SerializerFactory { } private boolean isCollection(CtClass beanClass) throws NotFoundException { - List interfaces = new ArrayList(Arrays.asList(beanClass.getInterfaces())); + List interfaces = new ArrayList(asList(beanClass.getInterfaces())); interfaces.add(beanClass); for (CtClass interfaze : interfaces) { if (interfaze.getName().equals(COLLECTION) || interfaze.getName().equals(LIST) || interfaze.getName().equals(SET)) { @@ -215,14 +207,9 @@ public class SynthSerializerFactory implements SerializerFactory { } private boolean isMap(CtClass beanClass) throws NotFoundException { - List interfaces = new ArrayList(Arrays.asList(beanClass.getInterfaces())); + List interfaces = new ArrayList<>(asList(beanClass.getInterfaces())); interfaces.add(beanClass); - for (CtClass interfaze : interfaces) { - if (interfaze.getName().equals(MAP)) { - return true; - } - } - return false; + return interfaces.stream().anyMatch(i -> mapInterfaces.contains(i.getName())); } /* @@ -346,22 +333,22 @@ public class SynthSerializerFactory implements SerializerFactory { String asPrimitive(String name) { switch (name) { - case "int": - return "I"; - case "byte": - return "B"; - case "float": - return "F"; - case "long": - return "J"; - case "boolean": - return "Z"; - case "char": - return "C"; - case "double": - return "D"; - case "short": - return "S"; + case "int": + return "I"; + case "byte": + return "B"; + case "float": + return "F"; + case "long": + return "J"; + case "boolean": + return "Z"; + case "char": + return "C"; + case "double": + return "D"; + case "short": + return "S"; } return ""; } @@ -369,8 +356,4 @@ public class SynthSerializerFactory implements SerializerFactory { String innerClassName(String name) { return "L" + name.replaceAll("\\.", "/").replaceAll("\\[\\]", ""); } - - static boolean isPrimitiveOrWrapperOrString(CtClass beanClass) { - return beanClass.isPrimitive() || wrappersAndString.contains(beanClass.getName()); - } } diff --git a/src/main/resources/index.html b/src/main/resources/index.html index b4ca674..4d49e1f 100644 --- a/src/main/resources/index.html +++ b/src/main/resources/index.html @@ -10,9 +10,14 @@
+ diff --git a/src/main/resources/perfix.js b/src/main/resources/perfix.js index a1fff23..943dbe5 100644 --- a/src/main/resources/perfix.js +++ b/src/main/resources/perfix.js @@ -1,4 +1,5 @@ function tabulate(data, columns) { + d3.select('table').remove(); var table = d3.select('body').append('table') var thead = table.append('thead') var tbody = table.append('tbody'); diff --git a/src/main/resources/tree.js b/src/main/resources/tree.js new file mode 100644 index 0000000..be0123e --- /dev/null +++ b/src/main/resources/tree.js @@ -0,0 +1,141 @@ +var m = [20, 120, 20, 120], + w = 1280 - m[1] - m[3], + h = 800 - m[0] - m[2], + i = 0, + root; + +var tree = d3.layout.tree() + .size([h, w]); + +var diagonal = d3.svg.diagonal() + .projection(function(d) { return [d.y, d.x]; }); + +var vis = d3.select("#body").append("svg:svg") + .attr("width", w + m[1] + m[3]) + .attr("height", h + m[0] + m[2]) + .append("svg:g") + .attr("transform", "translate(" + m[3] + "," + m[0] + ")"); + +d3.json("flare.json", function(json) { + root = json; + root.x0 = h / 2; + root.y0 = 0; + + function toggleAll(d) { + if (d.children) { + d.children.forEach(toggleAll); + toggle(d); + } + } + + // Initialize the display to show a few nodes. + root.children.forEach(toggleAll); + toggle(root.children[1]); + toggle(root.children[1].children[2]); + toggle(root.children[9]); + toggle(root.children[9].children[0]); + + update(root); +}); + +function update(source) { + var duration = d3.event && d3.event.altKey ? 5000 : 500; + + // Compute the new tree layout. + var nodes = tree.nodes(root).reverse(); + + // Normalize for fixed-depth. + nodes.forEach(function(d) { d.y = d.depth * 180; }); + + // Update the nodes… + var node = vis.selectAll("g.node") + .data(nodes, function(d) { return d.id || (d.id = ++i); }); + + // Enter any new nodes at the parent's previous position. + var nodeEnter = node.enter().append("svg:g") + .attr("class", "node") + .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) + .on("click", function(d) { toggle(d); update(d); }); + + nodeEnter.append("svg:circle") + .attr("r", 1e-6) + .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); + + nodeEnter.append("svg:text") + .attr("x", function(d) { return d.children || d._children ? -10 : 10; }) + .attr("dy", ".35em") + .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; }) + .text(function(d) { return d.name; }) + .style("fill-opacity", 1e-6); + + // Transition nodes to their new position. + var nodeUpdate = node.transition() + .duration(duration) + .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }); + + nodeUpdate.select("circle") + .attr("r", 4.5) + .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); + + nodeUpdate.select("text") + .style("fill-opacity", 1); + + // Transition exiting nodes to the parent's new position. + var nodeExit = node.exit().transition() + .duration(duration) + .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; }) + .remove(); + + nodeExit.select("circle") + .attr("r", 1e-6); + + nodeExit.select("text") + .style("fill-opacity", 1e-6); + + // Update the links… + var link = vis.selectAll("path.link") + .data(tree.links(nodes), function(d) { return d.target.id; }); + + // Enter any new links at the parent's previous position. + link.enter().insert("svg:path", "g") + .attr("class", "link") + .attr("d", function(d) { + var o = {x: source.x0, y: source.y0}; + return diagonal({source: o, target: o}); + }) + .transition() + .duration(duration) + .attr("d", diagonal); + + // Transition links to their new position. + link.transition() + .duration(duration) + .attr("d", diagonal); + + // Transition exiting nodes to the parent's new position. + link.exit().transition() + .duration(duration) + .attr("d", function(d) { + var o = {x: source.x, y: source.y}; + return diagonal({source: o, target: o}); + }) + .remove(); + + // Stash the old positions for transition. + nodes.forEach(function(d) { + d.x0 = d.x; + d.y0 = d.y; + }); +} + +// Toggle children. +function toggle(d) { + if (d.children) { + d._children = d.children; + d.children = null; + } else { + d.children = d._children; + d._children = null; + } +} + diff --git a/src/deploy/java/testperfix/Main.java b/src/testapp/java/testperfix/cmdline/App.java similarity index 97% rename from src/deploy/java/testperfix/Main.java rename to src/testapp/java/testperfix/cmdline/App.java index 3343450..d904906 100644 --- a/src/deploy/java/testperfix/Main.java +++ b/src/testapp/java/testperfix/cmdline/App.java @@ -1,9 +1,9 @@ -package testperfix; +package testperfix.cmdline; import java.sql.*; import java.util.concurrent.TimeUnit; -public class Main { +public class App { public static void main(String[] args) { System.out.println("Perfix Test Application is running. Make sure the agent is active."); String includesProperty = System.getProperty("perfix.includes"); diff --git a/src/testapp/java/testperfix/web/WebApp.java b/src/testapp/java/testperfix/web/WebApp.java new file mode 100644 index 0000000..228c45d --- /dev/null +++ b/src/testapp/java/testperfix/web/WebApp.java @@ -0,0 +1,34 @@ +package testperfix.web; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication +@RestController +public class WebApp { + public static void main(String[] args) { + String includesProperty = System.getProperty("perfix.includes"); + if (includesProperty == null || !includesProperty.equals("testperfix")) { + System.out.println("Start me with -javaagent:target/agent-0.1-SNAPSHOT.jar -Dperfix.includes=testperfix"); + + System.out.println("Exiting now"); + System.exit(0); + } + + SpringApplication.run(WebApp.class, args); + System.out.println("Perfix Test Web Application is running"); + } + + + @RequestMapping("/greetings") + public String index() { + return greetings(); + } + + private String greetings() { + return "Greetings from Spring Boot!"; + } + +}