27-Second Hack To Install A Java Agent with JAVA_TOOL_OPTIONS
If you’re developing a Java application, you’re likely using an APM, an exception management tool or a tracing solution. For any of those tools, you have probably faced the common challenge of monitoring a Java application: installing an instrumentation Agent.
Java monitoring is performed by instrumentation agents called Java Agents and VM Agents. Both of them are based on the same underlying framework- the JVMTI, best described in this article by Oracle. At Rookout, we use Java Agents to perform bytecode manipulation, allowing us to fetch debug messages for you wherever your code is deployed. That means we need to make sure our customers can easily install the Rookout Java Agent, and use it to run their code.
But as each of our customers uses a different flavor of Java, a different application server, and a different build tool, we soon found that installing a Java Agent and using it to instrument the customer’s JVM doesn’t have a simple, straightforward implementation.
I recently set out on a mission to find an easy solution that would work across Java configurations. Since my journey had a happy outcome, I decided to share it with you in this blog post.
Quest accepted
Supposedly the simplest way to add a java agent is by adding this command line argument to the JVM:
java -javaagent:agent.jar MyClass.class
For native agents, you’ll use `-agentpath` or`-agentlib`. Although for the rest of this post, we’ll focus on Java agents, everything here applies to native agents as well.
Unfortunately, more often than not, you don’t execute the JVM directly. Due to the complex nature of configuring JVM command lines, you usually use some wrapper shell script (`.sh` for Linux and Mac, `.bat` for Windows) to execute the JVM with the correct command line for you.
In other words, finding that “java -javaagent:…” line above and editing it is not trivial at all.
A second stab at the dragon
I’m sure many of you are familiar with JAVA_OPTS. You probably think “well, that’s easy enough, I’ll just set the options I need there:”
JAVA_OPTS=-javaagent:agent.jar wrapper.sh
While at times this works well, JAVA_OPTS is nothing more than a convention, and most tools do not adhere to it. For example, Maven ignores JAVA_OPTS in favor of MAVEN_OPTS.
In time we realized that finding the appropriate configuration for each tool is a never-ending chase.
Further down the rabbit hole
Making matters even worse is the fact that in some cases, multiple JVMs might be invoked, each with its own set of arguments. For example, build tools such as Gradle or Maven invoke a second JVM to execute an application or run tests, with their set of arguments. To add an agent to one of those JVMs, you would have to edit your Gradle file with something like:
test {
jvmArgs = ["-javaagent:${agent.jar}"]
}
So even if you were able to use any of the methods mentioned above, it wouldn’t necessarily apply to the instance you are trying to monitor or debug.
The wisdom of the ancients
Confused? So was I!
That’s when I stumbled onto this ancient blog post by my friend Demi Ben-Ari.
The wisdom of the ancients has led me to a simple and straightforward solution that works across environments and build tools. All you need to do is use this hidden JVM environment variable:
export JAVA_TOOL_OPTIONS=-javaagent:agent.jar
You just set it in your Shell, on your Docker, or even on your entire (Virtual) Machine, and you’re golden! 🙂
Plus it should take you just under 27 seconds. We counted.
By the way, if you want to read more about the difference between _JAVA_OPTIONS and JAVA_TOOL_OPTIONS and what should you use, check out this StackOverflow gem.
And the other holy grail
We did encounter some cases in which even this magical solution doesn’t apply. For example, using a Java Agent in a Serverless or PaaS deployment is near impossible. And in some organizations, the developer trying to install the Rookout Java Agent doesn’t have the permissions required for changing environment variables, or for directly accessing the file system. To address such cases we also provided a way of setting up the Rookout SDK using an API.
So if all else fails, you may want to consider providing an API as well, and this article may be a good place to start.
All is well that ends well
Providing our customers with an easily installable Java Agent used to be a challenge. Many customers had trouble configuring the Java Agent, and we kept chasing different deployment methods to match each customer’s specific needs. Happily, we eventually found out about JAVA_TOOL_OPTIONS. Now, after complementing it with an API alternative, we can quickly cover the needs of our customers looking to debug a Java application.
We hope these methods will make your life easier as well!