preparedstatement recording
This commit is contained in:
parent
6459f35306
commit
7875c68ec3
9 changed files with 419 additions and 169 deletions
31
README.md
31
README.md
|
|
@ -1,15 +1,32 @@
|
||||||
# perfix
|
# perfix
|
||||||
Pretty basic profiling tool for JVM's
|
Pretty basic profiling tool for JVM's
|
||||||
|
|
||||||
* agent that instruments loaded classes: -javaagent:perfix.jar
|
# Highlights:
|
||||||
* include classes for instrumentation with -Dperfix.includes=com.project. ...etc (includes subpackages)
|
* Meant for development time (after process stops, data is gone).
|
||||||
* ssh interface to report executed methods:
|
* Minimal memory footprint (agent is 2.5 mb).
|
||||||
|
* Minimalistic commandline interface.
|
||||||
|
* Execution time is measured in nanoseconds, reported in milliseconds (this way the totals and averages are most precise, but also human readable).
|
||||||
|
* No manual instrumentation necessary using loadtime bytecode manipulation (javassist).
|
||||||
|
* No special jdbc configuration necessary (ie no wrapped jdbc driver).
|
||||||
|
* The agent is also the server (unlike commercial tooling). This way there is no overhead in interprocess communication.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
* Agent that instruments loaded classes: -javaagent:<path>/perfix.jar
|
||||||
|
* Include classes for instrumentation with -Dperfix.includes=com.project. ...etc (includes subpackages)
|
||||||
|
* Ssh interface to report executed methods and sql query excutions:
|
||||||
<br/> * #invocations
|
<br/> * #invocations
|
||||||
<br/> * total execution time for the method in nanoseconds (this is also the sorting order)
|
<br/> * total execution time for the method in nanoseconds (this is also the sorting order)
|
||||||
<br/> * average time in nanoseconds per method (= total/#invocations)
|
<br/> * average time in nanoseconds per method (= total/#invocations)
|
||||||
* The server starts on port 2048 by default. Use -Dperfix.port=... to adjust.
|
* The (ssh) server starts on port 2048 by default. Use -Dperfix.port=... to adjust.
|
||||||
|
|
||||||
|
|
||||||
# roadmap
|
# roadmap
|
||||||
* finish jdbc query logging
|
* Overhead (in method execution time) not clear yet. I wouldn't use it in production.
|
||||||
* make output format configurable
|
* Finish jdbc query logging
|
||||||
* implement password login (now any)
|
* Make output format configurable
|
||||||
|
* Implement password login (now any)
|
||||||
|
* Add web interface (maybe)
|
||||||
|
* Implement an actual call stack the way commercial tools work
|
||||||
|
|
||||||
|
# DISCLAIMER:
|
||||||
|
This has only been tested on oracle java8 in spring-boot using tomcat web-container
|
||||||
|
|
|
||||||
24
agent.iml
Normal file
24
agent.iml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
|
||||||
|
<output url="file://$MODULE_DIR$/target/classes" />
|
||||||
|
<output-test url="file://$MODULE_DIR$/target/test-classes" />
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/deploy/java" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
<orderEntry type="library" name="Maven: org.ow2.asm:asm:4.1" level="project" />
|
||||||
|
<orderEntry type="library" name="Maven: org.javassist:javassist:3.21.0-GA" level="project" />
|
||||||
|
<orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" />
|
||||||
|
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
|
||||||
|
<orderEntry type="library" scope="TEST" name="Maven: com.h2database:h2:1.4.195" level="project" />
|
||||||
|
<orderEntry type="library" name="Maven: org.apache.sshd:sshd-core:1.7.0" level="project" />
|
||||||
|
<orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.25" level="project" />
|
||||||
|
<orderEntry type="library" name="Maven: org.slf4j:slf4j-nop:1.7.25" level="project" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
10
pom.xml
10
pom.xml
|
|
@ -29,12 +29,18 @@
|
||||||
<groupId>com.h2database</groupId>
|
<groupId>com.h2database</groupId>
|
||||||
<artifactId>h2</artifactId>
|
<artifactId>h2</artifactId>
|
||||||
<version>1.4.195</version>
|
<version>1.4.195</version>
|
||||||
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.sshd</groupId>
|
<groupId>org.apache.sshd</groupId>
|
||||||
<artifactId>sshd-core</artifactId>
|
<artifactId>sshd-core</artifactId>
|
||||||
<version>1.7.0</version>
|
<version>1.7.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-nop</artifactId>
|
||||||
|
<version>1.7.25</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
|
@ -43,9 +49,12 @@
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
<testSourceDirectory>src/deploy/java</testSourceDirectory>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-jar-plugin</artifactId>
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<archive>
|
<archive>
|
||||||
<manifestEntries>
|
<manifestEntries>
|
||||||
|
|
@ -58,6 +67,7 @@
|
||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.1.1</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<phase>package</phase>
|
<phase>package</phase>
|
||||||
|
|
|
||||||
|
|
@ -5,31 +5,49 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
System.out.println("Start me with -javaagent:target/agent-0.1-SNAPSHOT.jar -Dperfix.includes=testperfix");
|
System.out.println("Perfix Test Application is running. Make sure the agent is active.");
|
||||||
System.out.println("Then start putty (or other telnet client) and telnet to localhost:2048");
|
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);
|
||||||
|
}
|
||||||
|
System.out.println("Now start ssh session on localhost on port 2048 (without closing the Test Application)");
|
||||||
run();
|
run();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void run() {
|
private static void run() {
|
||||||
someJdbcMethod();
|
|
||||||
someOtherMethod();
|
|
||||||
try {
|
try {
|
||||||
|
Class.forName("org.h2.Driver");
|
||||||
|
someJdbcStatentMethod();
|
||||||
|
someJdbcPreparedStatementMethod();
|
||||||
|
someOtherMethod();
|
||||||
TimeUnit.SECONDS.sleep(1);
|
TimeUnit.SECONDS.sleep(1);
|
||||||
} catch (InterruptedException e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void someJdbcMethod() {
|
private static void someJdbcStatentMethod() {
|
||||||
try {
|
try {
|
||||||
Class.forName("org.h2.Driver");
|
|
||||||
Connection connection = DriverManager.getConnection("jdbc:h2:mem:default", "sa", "");
|
Connection connection = DriverManager.getConnection("jdbc:h2:mem:default", "sa", "");
|
||||||
Statement statement = connection.createStatement();
|
Statement statement = connection.createStatement();
|
||||||
ResultSet resultSet = statement.executeQuery("select CURRENT_DATE()");
|
statement.executeQuery("select CURRENT_DATE() -- simple statement");
|
||||||
while (resultSet.next()){
|
connection.close();
|
||||||
System.out.println("today is "+resultSet.getObject(1));
|
} catch (SQLException e) {
|
||||||
}
|
e.printStackTrace();
|
||||||
} catch (ClassNotFoundException | SQLException e) {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void someJdbcPreparedStatementMethod() {
|
||||||
|
try {
|
||||||
|
Connection connection = DriverManager.getConnection("jdbc:h2:mem:default", "sa", "");
|
||||||
|
|
||||||
|
PreparedStatement preparedStatement = connection.prepareStatement("select CURRENT_DATE() -- prepared statement");
|
||||||
|
preparedStatement.executeQuery();
|
||||||
|
connection.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import perfix.server.SSHServer;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.instrument.Instrumentation;
|
import java.lang.instrument.Instrumentation;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
|
|
@ -17,149 +18,26 @@ public class Agent {
|
||||||
private static final String INCLUDES_PROPERTY = "perfix.includes";
|
private static final String INCLUDES_PROPERTY = "perfix.includes";
|
||||||
|
|
||||||
private static final String DEFAULT_PORT = "2048";
|
private static final String DEFAULT_PORT = "2048";
|
||||||
private static final String MESSAGE = "Perfix agent active";
|
private static final String MESSAGE = " --- Perfix agent active --- ";
|
||||||
|
|
||||||
private static final String PERFIX_METHODINVOCATION_CLASS = "perfix.MethodInvocation";
|
|
||||||
|
|
||||||
private static ClassPool classpool;
|
|
||||||
|
|
||||||
public static void premain(String agentArgs, Instrumentation inst) {
|
public static void premain(String agentArgs, Instrumentation inst) {
|
||||||
System.out.println(MESSAGE);
|
System.out.println(MESSAGE);
|
||||||
|
|
||||||
int port = Integer.parseInt(System.getProperty(PORT_PROPERTY, DEFAULT_PORT));
|
int port = Integer.parseInt(System.getProperty(PORT_PROPERTY, DEFAULT_PORT));
|
||||||
|
|
||||||
classpool = ClassPool.getDefault();
|
new ClassInstrumentor(determineIncludes()).instrumentCode(inst);
|
||||||
|
|
||||||
instrumentCode(inst);
|
|
||||||
|
|
||||||
new SSHServer().startListeningOnSocket(port);
|
new SSHServer().startListeningOnSocket(port);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void instrumentCode(Instrumentation inst) {
|
|
||||||
List<String> includes = determineIncludes();
|
|
||||||
|
|
||||||
inst.addTransformer((classLoader, resource, aClass, protectionDomain, uninstrumentedByteCode)
|
|
||||||
-> createByteCode(includes, resource, uninstrumentedByteCode));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] createByteCode(List<String> includes, String resource, byte[] uninstrumentedByteCode) {
|
|
||||||
if (!isInnerClass(resource)) {
|
|
||||||
try {
|
|
||||||
CtClass ctClass = getCtClassForResource(resource);
|
|
||||||
if (isJdbcStatement(resource, ctClass)) {
|
|
||||||
return instrumentJdbcCalls(ctClass);
|
|
||||||
}
|
|
||||||
if (shouldInclude(resource, includes)) {
|
|
||||||
byte[] instrumentedBytecode = instrumentMethod(ctClass);
|
|
||||||
if (instrumentedBytecode != null) {
|
|
||||||
return instrumentedBytecode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
//suppress
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return uninstrumentedByteCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] instrumentJdbcCalls(CtClass classToInstrument) throws IOException, CannotCompileException {
|
|
||||||
try {
|
|
||||||
stream(classToInstrument.getDeclaredMethods("executeQuery")).forEach(m -> {
|
|
||||||
try {
|
|
||||||
m.insertBefore("System.out.println($1);");
|
|
||||||
} catch (CannotCompileException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
byte[] byteCode = classToInstrument.toBytecode();
|
|
||||||
classToInstrument.detach();
|
|
||||||
return byteCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isJdbcStatement(String resource, CtClass ctClass) throws NotFoundException {
|
|
||||||
if (!resource.startsWith("java/sql")) {
|
|
||||||
return stream(ctClass.getInterfaces())
|
|
||||||
.anyMatch(i -> i.getName().equals("java.sql.Statement") && !i.getName().equals("java.sql.PreparedStatement"));
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] instrumentMethod(CtClass classToInstrument) throws
|
|
||||||
NotFoundException, IOException, CannotCompileException {
|
|
||||||
|
|
||||||
CtClass perfixMethodInvocationClass = getCtClass(PERFIX_METHODINVOCATION_CLASS);
|
|
||||||
|
|
||||||
if (!classToInstrument.isInterface()) {
|
|
||||||
stream(classToInstrument.getDeclaredMethods()).forEach(m -> {
|
|
||||||
instrumentMethod(perfixMethodInvocationClass, m);
|
|
||||||
});
|
|
||||||
byte[] byteCode = classToInstrument.toBytecode();
|
|
||||||
classToInstrument.detach();
|
|
||||||
return byteCode;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CtClass getCtClassForResource(String resource) throws NotFoundException {
|
|
||||||
return getCtClass(resource.replaceAll("/", "."));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CtClass getCtClass(String classname) throws NotFoundException {
|
|
||||||
return classpool.get(classname);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void instrumentMethod(CtClass methodClass, CtMethod methodToinstrument) {
|
|
||||||
try {
|
|
||||||
methodToinstrument.addLocalVariable("perfixmethod", methodClass);
|
|
||||||
methodToinstrument.insertBefore("perfixmethod = perfix.MethodInvocation.start(\"" + methodToinstrument.getLongName() + "\");");
|
|
||||||
methodToinstrument.insertAfter("perfixmethod.stop();");
|
|
||||||
} catch (CannotCompileException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> determineIncludes() {
|
private static List<String> determineIncludes() {
|
||||||
return new ArrayList<>(asList(System.getProperty(INCLUDES_PROPERTY).split(",")));
|
String includesPropertyValue = System.getProperty(INCLUDES_PROPERTY);
|
||||||
}
|
if (includesPropertyValue==null){
|
||||||
|
System.out.println("WARNING: perfix.includes not set ");
|
||||||
private static boolean shouldInclude(String resource, List<String> excludes) {
|
return Collections.emptyList();
|
||||||
BooleanWrapper included = new BooleanWrapper(false);
|
|
||||||
excludes.forEach(include -> {
|
|
||||||
if (resource.startsWith(include)) {
|
|
||||||
included.set(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return included.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isInnerClass(String resource) {
|
|
||||||
return resource.contains("$");
|
|
||||||
}
|
|
||||||
|
|
||||||
static class BooleanWrapper {
|
|
||||||
boolean value;
|
|
||||||
|
|
||||||
BooleanWrapper(boolean value) {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
void set(boolean value) {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean get() {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return Boolean.toString(value);
|
|
||||||
}
|
}
|
||||||
|
return new ArrayList<>(asList(includesPropertyValue.split(",")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
284
src/main/java/perfix/ClassInstrumentor.java
Normal file
284
src/main/java/perfix/ClassInstrumentor.java
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
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<String> includes;
|
||||||
|
private CtClass perfixMethodInvocationClass;
|
||||||
|
private CtClass stringClass;
|
||||||
|
|
||||||
|
ClassInstrumentor(List<String> 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<String> 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<String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,13 +13,16 @@ public class MethodInvocation {
|
||||||
public static MethodInvocation start(String name) {
|
public static MethodInvocation start(String name) {
|
||||||
return new MethodInvocation(name);
|
return new MethodInvocation(name);
|
||||||
}
|
}
|
||||||
|
public static void stop(MethodInvocation methodInvocation) {
|
||||||
|
methodInvocation.stop();
|
||||||
|
}
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
t1 = System.nanoTime();
|
t1 = System.nanoTime();
|
||||||
Registry.add(this);
|
Registry.add(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
String getName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
22
src/main/java/perfix/MutableBoolean.java
Normal file
22
src/main/java/perfix/MutableBoolean.java
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package perfix;
|
||||||
|
|
||||||
|
public class MutableBoolean {
|
||||||
|
private boolean value;
|
||||||
|
|
||||||
|
MutableBoolean(boolean value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set(boolean value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean get() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return Boolean.toString(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,35 +11,31 @@ import java.io.PrintStream;
|
||||||
|
|
||||||
public class SshSessionInstance implements Command, Runnable {
|
public class SshSessionInstance implements Command, Runnable {
|
||||||
|
|
||||||
private static final String ANSI_LOCAL_ECHO = "\u001B[12l";
|
|
||||||
private static final String ANSI_NEWLINE_CRLF = "\u001B[20h";
|
private static final String ANSI_NEWLINE_CRLF = "\u001B[20h";
|
||||||
|
|
||||||
private InputStream is;
|
private InputStream input;
|
||||||
private OutputStream os;
|
private OutputStream output;
|
||||||
|
|
||||||
private ExitCallback callback;
|
private ExitCallback callback;
|
||||||
private Thread sshThread;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start(Environment env) {
|
public void start(Environment env) {
|
||||||
sshThread = new Thread(this, "EchoShell");
|
new Thread(this, "PerfixShell").start();
|
||||||
sshThread.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
os.write("press [enter] for report or [q] to quit\n".getBytes());
|
output.write("press [enter] for report or [q] to quit\n".getBytes());
|
||||||
os.write((ANSI_LOCAL_ECHO + ANSI_NEWLINE_CRLF).getBytes());
|
output.write((ANSI_NEWLINE_CRLF).getBytes());
|
||||||
os.flush();
|
output.flush();
|
||||||
|
|
||||||
boolean exit = false;
|
boolean exit = false;
|
||||||
while (!exit) {
|
while (!exit) {
|
||||||
char c = (char) is.read();
|
char c = (char) input.read();
|
||||||
if (c == 'q') {
|
if (c == 'q') {
|
||||||
exit = true;
|
exit = true;
|
||||||
} else if (c == '\n') {
|
} else if (c == '\n') {
|
||||||
Registry.report(new PrintStream(os));
|
Registry.report(new PrintStream(output));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
@ -50,9 +46,7 @@ public class SshSessionInstance implements Command, Runnable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void destroy() {
|
public void destroy() { }
|
||||||
sshThread.interrupt();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setErrorStream(OutputStream errOS) {
|
public void setErrorStream(OutputStream errOS) {
|
||||||
|
|
@ -65,12 +59,12 @@ public class SshSessionInstance implements Command, Runnable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setInputStream(InputStream is) {
|
public void setInputStream(InputStream is) {
|
||||||
this.is = is;
|
this.input = is;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setOutputStream(OutputStream os) {
|
public void setOutputStream(OutputStream os) {
|
||||||
this.os = os;
|
this.output = os;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue