JIB is a lightweight Java agent designed to seamlessly instrument functions in compiled Java applications at the bytecode level. Leveraging the power of Byte Buddy as its core, JIB offers efficient, lightweight, and application-level function instrumentation with minimal overhead.
- Non-intrusive bytecode manipulation
- Configurable instrumentation targets
- Flexible logging options
- Method-level granularity for instrumentation
- Seamless integration with existing Java applications
- Compatible with Java 8 and above
To build and install JIB, ensure you have Maven installed on your system. Then, execute the following command in the project root directory:
mvn clean install
This command compiles the source code, runs tests, and installs the artifact in your local Maven repository.
To use JIB as an agent attached to your Java application, you'll need to package it as a JAR file. Generate the JAR with this command:
mvn clean package
Upon successful execution, you'll find the jib.jar
file in the /target
directory, ready for deployment.
You can attach the agent to your Java application when executing it. Here is a sample structure for attaching the agent to your Java program.
# Running the program normally
java [OPTIONS] -jar <program commands>
# Attaching the agent to your program
java [OPTIONS] -javaagent:path/to/the/jib.jar=<config=configuration.yaml> -jar <program commands>
The agent configuration is specified in a YAML file. This file allows you to customize various aspects of the agent's behavior. Below are the available configuration options:
The logging
section controls how the agent generates log files:
-
file
: Specifies the path to the log file where the agent will write its output.- Default:
app.log
- Default:
-
addTimestampToFileNames
: When set totrue
, adds a timestamp to the log file name.- Default:
false
- Default:
-
useHash
: Iftrue
, the agent uses hashing for method signatures in the log file. This is useful for reducing the size of the log file when there are a large number of method logs with long signatures. The mapping of the hashes will be stored in a separate JSON file.- Default:
false
- Default:
-
optimizeTimestamp
: When set totrue
, optimizes timestamp handling for improved performance. Basically, it removes the first 4 digits of the timestamp to reduce the log file size.- Default:
false
- Default:
The instrumentation
section defines which parts of your code the agent will instrument:
-
targetPackage
: Specifies which package to instrument.- Default:
*
(all packages)
- Default:
-
onlyCheckVisited
: When set totrue
, the agent only instruments the entry of each function once. This is useful for checking code coverage.- Default:
false
- Default:
-
instrumentMainMethod
: Iftrue
, the agent will instrument the main method of the main class.- Default:
false
- Default:
-
maxNumberOfInstrumentations
: Sets a limit on the number of instrumentations to perform. Use -1 for unlimited.- Default:
-1
- Default:
-
targetMethods
: Allows you to specify which methods to instrument or ignore. This section has two sub-sections:instrument
: A list of methods to instrument. If specified, only these methods will be instrumented.- Example:
instrument: - private static void com.example.MainClass.methodName(int a, java.lang.String b)
- Example:
ignore
: A list of methods to exclude from instrumentation. If specified, all methods except these will be instrumented.- Example:
ignore: - protected java.lang.String com.example.MainClass.ignoredMethodName(float a)
- Example:
The misc
section contains additional configuration options:
convertToJson
: When set totrue
, converts the output logs to JSON format. This format is supported by visualization tools like Eclipse Trace Compass.- Default:
false
- Default:
When specifying methods in the instrument
or ignore
lists, use the following format:
[visibility] [static] return-type [declaring-class.]method-name(args)
Where:
[visibility]
is one of (required):public
protected
private
- (empty for package-protected)
[static]
is one of (required):static
- (empty for non-static)
return-type
is the method's return type- it should be the fully qualified class name (e.g.,
java.lang.String
orvoid
) (required)
- it should be the fully qualified class name (e.g.,
[declaring-class]
is the fully qualified class name where the method is declared (optional)method-name
is the name of the method (required)(args)
are the method's parameters (required)
Examples:
private static void com.example.MainClass.methodName(int a, java.lang.String b)
protected java.lang.String ignoredMethodName(float a)
public java.util.List<java.lang.String> getNames()
void processData(byte[] data)
Note: You can specify either instrument
or ignore
, but not both since it doesn't make sense to instrument and ignore methods at the same time.
Here's an example of a complete configuration file:
logging:
file: app.log
addTimestampToFileNames: true
useHash: true
optimizeTimestamp: true
instrumentation:
targetPackage: com.example
onlyCheckVisited: false
instrumentMainMethod: true
maxNumberOfInstrumentations: 1000
targetMethods:
instrument:
- private static void com.example.MainClass.methodName(int a, java.lang.String b)
misc:
convertToJson: true
Let's consider a simple Java program with the following structure:
package com.example.pkg;
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
firstMethod();
Main main = new Main();
main.secondMethod();
main.thirdMethod(1, 2);
main.fourthMethod();
fifthMethod(new HashMap<String, Integer>(), "key");
}
private static void firstMethod() {
System.out.println("First Method");
}
String secondMethod() {
return "Second Method";
}
public int thirdMethod(int a, int b) {
return a + b;
}
protected Another fourthMethod() {
return new Another();
}
private static void fifthMethod(HashMap<String, Integer> map, String key) {
if (map.containsKey(key)) {
System.out.println("Key exists");
} else {
System.out.println("Key does not exist");
}
return;
}
}
Suppose we want to instrument the program to log the entry and exit of each method. We can create a YAML configuration file to specify the methods to instrument and the logging options. For now, let's just instrument all the methods in the com.example.pkg
package and log the output to a file.
logging:
file: app.log
instrumentation:
targetPackage: com.example.pkg
After the instrumentation, the agent will store all the logs in a single file (i.e., app.log
). Each line shows a function entry or exit along with the S/E (start or end) time and the function's signature.
[TIME_NANO_SECONDS] S|E FUNCTION_SIGNATURE
[1724605175635717866] S public static void com.example.pkg.Main.main(java.lang.String[])
[1724605175637779321] S private static void com.example.pkg.Main.firstMethod()
[1724605175637903541] E private static void com.example.pkg.Main.firstMethod()
[1724605175637944911] S java.lang.String com.example.pkg.Main.secondMethod()
[1724605175637971154] E java.lang.String com.example.pkg.Main.secondMethod()
[1724605175637995449] S public int com.example.pkg.Main.thirdMethod(int,int)
[1724605175638028792] E public int com.example.pkg.Main.thirdMethod(int,int)
[1724605175638069161] S protected com.example.pkg.Another com.example.pkg.Main.fourthMethod()
[1724605175640295270] E protected com.example.pkg.Another com.example.pkg.Main.fourthMethod()
[1724605175640359832] S private static void com.example.pkg.Main.fifthMethod(java.util.HashMap,java.lang.String)
[1724605175640388900] E private static void com.example.pkg.Main.fifthMethod(java.util.HashMap,java.lang.String)
[1724605175640410797] E public static void com.example.pkg.Main.main(java.lang.String[])
Now, we may specify the methods to instrument or ignore in the configuration file. For instance, we can instrument only the main
method and the thirdMethod
in the Main
class:
logging:
file: app.log
instrumentation:
targetPackage: com.example.pkg
instrumentMainMethod: true
targetMethods:
instrument:
- public int com.example.pkg.Main.thirdMethod(int a, int b)
After running the program with this configuration, the log file will only contain entries for the main
and thirdMethod
functions.
[1724605175635717866] S public static void com.example.pkg.Main.main(java.lang.String[])
[1724605175637995449] S public int com.example.pkg.Main.thirdMethod(int,int)
[1724605175638028792] E public int com.example.pkg.Main.thirdMethod(int,int)
[1724605175640410797] E public static void com.example.pkg.Main.main(java.lang.String[])
If you want to use hashing for method signatures in the log file, you can enable the useHash
option in the configuration file. This option is useful when there are a large number of method logs with long signatures.
logging:
file: app.log
useHash: true
instrumentation:
targetPackage: com.example.pkg
After running the program with this configuration, the log file will contain hashed method signatures instead of the full method signatures. The mapping of the hashes will be stored in a separate JSON file.
-
Log file:
[1724685951931233870] S A [1724685951933340359] S B [1724685951933525189] E B [1724685951933581572] S C [1724685951933615793] E C [1724685951933641228] S D [1724685951933668752] E D [1724685951933730567] S E [1724685951936219996] E E [1724685951936283964] S F [1724685951936315634] E F [1724685951936338337] E A
-
Log metadata (
.json
){ "start_time": 1724685951888001220, "end_time": 1724685951936780901, "method_signature_hash": { "public int com.example.pkg.Main.thirdMethod(int,int)": "D", "java.lang.String com.example.pkg.Main.secondMethod()": "C", "protected com.example.pkg.Another com.example.pkg.Main.fourthMethod()": "E", "public static void com.example.pkg.Main.main(java.lang.String[])": "A", "private static void com.example.pkg.Main.firstMethod()": "B", "private static void com.example.pkg.Main.fifthMethod(java.util.HashMap,java.lang.String)": "F" } }
There are several ways to analyze the logs generated by the agent. One common approach is to use visualization tools like Eclipse Trace Compass™ to gain insights into the program's execution flow and performance characteristics. You can also write custom scripts to parse and analyze the logs based on your specific requirements.
Background: Eclipse Trace Compass™ is an open source application to solve performance and reliability issues by reading and analyzing logs or traces of a system. Its goal is to provide views, graphs, metrics, and more to help extract useful information from traces, in a way that is more user-friendly and informative than huge text dumps.
If you want to import the collected instrumentation logs in Trace Compass, you can use the convertToJson
option in the configuration file to convert the output logs to JSON format.
JSON Output Example:
[
{
"ts": 1724686484544229.333,
"ph": "B",
"name": "public static void com.example.pkg.Main.main(java.lang.String[])"
},
{
"ts": 1724686484546125.563,
"ph": "B",
"name": "private static void com.example.pkg.Main.firstMethod()"
},
{
"ts": 1724686484546247.763,
"ph": "E",
"name": "private static void com.example.pkg.Main.firstMethod()"
},
{
"ts": 1724686484546282.093,
"ph": "B",
"name": "java.lang.String com.example.pkg.Main.secondMethod()"
},
{
"ts": 1724686484546306.258,
"ph": "E",
"name": "java.lang.String com.example.pkg.Main.secondMethod()"
},
{
"ts": 1724686484546331.966,
"ph": "B",
"name": "public int com.example.pkg.Main.thirdMethod(int,int)"
},
{
"ts": 1724686484546356.608,
"ph": "E",
"name": "public int com.example.pkg.Main.thirdMethod(int,int)"
},
{
"ts": 1724686484546380.330,
"ph": "B",
"name": "protected com.example.pkg.Another com.example.pkg.Main.fourthMethod()"
},
{
"ts": 1724686484548543.913,
"ph": "E",
"name": "protected com.example.pkg.Another com.example.pkg.Main.fourthMethod()"
},
{
"ts": 1724686484548604.858,
"ph": "B",
"name": "private static void com.example.pkg.Main.fifthMethod(java.util.HashMap,java.lang.String)"
},
{
"ts": 1724686484548632.518,
"ph": "E",
"name": "private static void com.example.pkg.Main.fifthMethod(java.util.HashMap,java.lang.String)"
},
{
"ts": 1724686484548654.718,
"ph": "E",
"name": "public static void com.example.pkg.Main.main(java.lang.String[])"
}
]
Then, you may import the trace file in Trace Compass. Below is a sample visualization of our trace file (i.e., Flame Chart
)
You can see this video tutorial on Youtube to see how to import the generated json file in Trace Compass.