I am attempting to add instrumentation to my Java application. I have successfully created agent code that I attach to my application via the "static" technique (e.g. https://www.baeldung.com/java-instrumentation). When I launch the application, the agent premain method is called and then the application executes. Unfortuanetly, premain is unable to find the target application class.
My application uses Spring Boot. I am suspecting that this may have something to do with the agent's inability to find the target class, but do not know.
I am not sure what I should provide as far as code, configuration, etc., to illustrate the state of the application. Please let me know what I should include to help.
Here is the exception that occurs when the agent attempts to find and transform the target class:
[INFO ] 2023-09-11 14:33:11.900 [main] com.beastcode.devops.prometheus.InstrumentationAgent.premain(InstrumentationAgent.java:28) - [Agent] In premain method
[INFO ] 2023-09-11 14:33:11.903 [main] com.beastcode.devops.prometheus.InstrumentationAgent.premain(InstrumentationAgent.java:29) - [Agent] args: com.beastcode.devops.prometheus.MetricsCollector:collect
[INFO ] 2023-09-11 14:33:11.904 [main] com.beastcode.devops.prometheus.Transformer.transform(Transformer.java:48) - [Agent] Transforming class com/beastcode/devops/prometheus/MetricsCollector
[ERROR] 2023-09-11 14:33:11.908 [main] com.beastcode.devops.prometheus.Transformer.transform(Transformer.java:77) - Exception
javassist.NotFoundException: com.beastcode.devops.prometheus.MetricsCollector
at javassist.ClassPool.get(ClassPool.java:436) ~[agent-1.0.0.jar:?]
at com.beastcode.devops.prometheus.Transformer.transform(Transformer.java:57) ~[agent-1.0.0.jar:?]
at java.lang.instrument.ClassFileTransformer.transform(ClassFileTransformer.java:246) ~[?:?]
at sun.instrument.TransformerManager.transform(TransformerManager.java:188) ~[?:?]
at sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:563) ~[?:?]
at sun.instrument.InstrumentationImpl.retransformClasses0(Native Method) ~[?:?]
at sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:167) ~[?:?]
at com.beastcode.devops.prometheus.InstrumentationAgent.transform(InstrumentationAgent.java:82) ~[agent-1.0.0.jar:?]
at com.beastcode.devops.prometheus.InstrumentationAgent.transformClass(InstrumentationAgent.java:57) ~[agent-1.0.0.jar:?]
at com.beastcode.devops.prometheus.InstrumentationAgent.premain(InstrumentationAgent.java:35) ~[agent-1.0.0.jar:?]
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:?]
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
at java.lang.reflect.Method.invoke(Method.java:566) ~[?:?]
at sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:513) ~[?:?]
at sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:525) ~[?:?]
...
com.beastcode.devops.prometheus.MetricsCollector is the target class and it certainly exists as this is the "main" application code that is started by Spring Boot. This can be seen later in the console log:
...
[INFO ] 2023-09-11 14:33:14.508 [main] com.beastcode.devops.prometheus.MetricsCollector.collectInstantHits(MetricsCollector.java:58) - Metrics information collected
[INFO ] 2023-09-11 14:33:14.508 [main] com.beastcode.devops.prometheus.MetricsCollector.collectInstantHits(MetricsCollector.java:59) - Querying and writing data ...
[INFO ] 2023-09-11 14:33:20.847 [main] com.beastcode.devops.prometheus.MetricsCollector.collectInstantHits(MetricsCollector.java:80) - Done
My pom.xml file uses the spring-boot-maven-plugin to handle building and packaging the application. I'm wondering if the class is not visible because perhaps I'm missing an option.
Here is the application pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.11</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.beast-code.devops</groupId>
<artifactId>prometheus-metrics-sampler</artifactId>
<version>1.1.2</version>
<properties>
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<slf4j.version>1.7.36</slf4j.version>
<log4j2.version>2.19.0</log4j2.version>
<prometheus-common.version>1.2.2</prometheus-common.version>
<gitlab-url>https://gitlab.phactory.beast-code.com</gitlab-url>
<gitlab-project-id>863</gitlab-project-id>
</properties>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>phactory-gitlab-maven</id>
<url>${gitlab-url}/api/v4/projects/${gitlab-project-id}/packages/maven</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.beast-code.devops</groupId>
<artifactId>prometheus-common</artifactId>
<version>${prometheus-common.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.beast-code.devops</groupId>
<artifactId>prometheus-common</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Here is the agent class:
package com.beastcode.devops.prometheus;
import java.lang.instrument.Instrumentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class InstrumentationAgent {
private static Logger logger = LoggerFactory.getLogger(InstrumentationAgent.class);
public static void premain(String agentArgs, Instrumentation instrumentation) {
logger.info("[Agent] In premain method");
logger.info("[Agent] args: {}", agentArgs);
String[] args = agentArgs.split(":");
String className = args[0];
String methodName = args[1];
transformClass(className, methodName, instrumentation);
}
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
logger.info("[Agent] In agentmain method");
logger.info("[Agent] args: {}", agentArgs);
String[] args = agentArgs.split(":");
String className = args[0];
String methodName = args[1];
transformClass(className, methodName, instrumentation);
}
private static void transformClass(String className, String methodName, Instrumentation instrumentation) {
Class<?> targetCls = null;
ClassLoader targetClassLoader = null;
// See if we can get the class using forName.
try {
targetCls = Class.forName(className);
targetClassLoader = targetCls.getClassLoader();
transform(targetCls, targetClassLoader, methodName, instrumentation);
return;
} catch (Exception e) {
logger.error("Class [{}] not found with Class.forName");
}
// Otherwise, iterate all loaded classes and find what we want.
for (Class<?> clazz : instrumentation.getAllLoadedClasses()) {
if (clazz.getName().equals(className)) {
targetCls = clazz;
targetClassLoader = targetCls.getClassLoader();
transform(targetCls, targetClassLoader, methodName, instrumentation);
return;
}
}
throw new RuntimeException("Failed to find class [" + className + "]");
}
private static void transform(Class<?> clazz, ClassLoader classLoader, String methodName,
Instrumentation instrumentation) {
Transformer transformer = new Transformer(clazz.getName(), classLoader, methodName);
instrumentation.addTransformer(transformer, true);
try {
instrumentation.retransformClasses(clazz);
} catch (Exception e) {
throw new RuntimeException("Transform failed for class: [" + clazz.getName() + "]", e);
}
}
}
Here is the transformer class:
package com.beastcode.devops.prometheus;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
public class Transformer implements ClassFileTransformer {
private static Logger logger = LoggerFactory.getLogger(Transformer.class);
// The internal form class name of the class to transform.
private final String targetClassName;
// The class loader of the class we want to transform.
private final ClassLoader targetClassLoader;
private final String methodName;
public Transformer(String targetClassName, ClassLoader targetClassLoader, String methodName) {
this.targetClassName = targetClassName;
this.targetClassLoader = targetClassLoader;
this.methodName = methodName;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
byte[] byteCode = classfileBuffer;
// Replace slashes (/) in target class name with periods (.) to construct a
// proper Java class name.
String finalTargetClassName = this.targetClassName.replaceAll("\\.", "/");
if (!className.equals(finalTargetClassName)) {
return byteCode;
}
if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {
logger.info("[Agent] Transforming class " + finalTargetClassName);
/*
* TODO This is logic specific to what you are trying to instrument into the
* class and method of interest.
*/
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get(targetClassName);
CtMethod mtd = cc.getDeclaredMethod(methodName);
mtd.addLocalVariable("startTime", CtClass.longType);
mtd.insertBefore("startTime = System.currentTimeMillis();");
StringBuilder endBlock = new StringBuilder();
mtd.addLocalVariable("endTime", CtClass.longType);
mtd.addLocalVariable("opTime", CtClass.longType);
endBlock.append("endTime = System.currentTimeMillis();");
endBlock.append("opTime = (endTime-startTime)/1000;");
endBlock.append("logger.info(\"[Application] Operation completed in:\" + opTime + \" seconds!\");");
mtd.insertAfter(endBlock.toString());
byteCode = cc.toBytecode();
cc.detach();
} catch (NotFoundException | CannotCompileException | IOException e) {
logger.error("Exception", e);
}
}
return byteCode;
}
}