Tracing Java applications on Linux using the LTTng-UST Java agent
Hi everyone! Alexandre here.
In this blog post, I will demonstrate how to generate LTTng traces from the Trace Compass viewer using the LTTng-UST Java agent integration. This should effectively explain:
- How to obtain LTTng traces from any Java application instrumented with
java.util.logging
tracepoints. - How to trace the Trace Compass application.
This is basically an update of Tutorial: Tracing Java Logging Frameworks for LTTng 2.7 and above. I will also go into more details regarding how to use configuration files and how to record Trace Compass traces.
Requirements
If you want to follow this post, make sure you have the following projects:
-
LTTng-UST ≥ 2.7 with the Java agent component.
If you build LTTng-UST from source, make sure you pass the
--enable-java-agent-jul
orenable-java-agent-all
option to theconfigure
script, which are not enabled by default.Most binary distributions also carry packages for the Java agent. For example, on Debian and Ubuntu, it is part of the
liblttng-ust-agent-java
package. -
Trace Compass ≥ 2.1 (currently only available in the development snapshots, or the Git master branch).
Try out the LTTng-UST Java agent
java.util.logging
(JUL) works by defining
Logger
and
Handler
objects. Loggers are part of the Java application: they receive log
calls once the execution reaches pre-determined log points. Handlers
are attached to loggers: they process the received log points to
generate and save log events wherever they are configured to do so (to a
file, to the console, over the network, or in our case, to an LTTng
trace).
The nice part is that, while the application has to define its loggers, it does not have to know about handlers at all. Those can be provided at run time, independently of the application being logged. More on this below.
There is a simple Java program example present in the LTTng-UST source tree. Let's take a look at it:
public class Hello {
/** Class-wide JUL logger object */
private static final Logger LOGGER = Logger.getLogger(Hello.class.getName());
public static void main(String args[]) throws IOException {
/* Instantiate a LTTngLogHandler object, and attach it to our logger */
Handler lttngHandler = new LttngLogHandler();
LOGGER.addHandler(lttngHandler);
/* Log events using the JUL Logger created before. */
LOGGER.info("Hello World, the answer is " + 42);
LOGGER.info("Another info event");
LOGGER.severe("A severe event");
/* Cleanup */
LOGGER.removeHandler(lttngHandler);
lttngHandler.close();
}
}
You can compile the example by going to the doc/examples/java-jul
and running make
. Then you can run it:
java -classpath /usr/local/share/java/*:. -Djava.library.path=/usr/local/lib Hello
In the last command line:
-classpath
indicates the class path, or where to look for Java classes. Here, I pointed to the location of thelttng-ust-agent-jul.jar
file, as well as.
sinceHello.class
is located in the current working directory.-Djava.library.path
is the equivalent ofLD_LIBRARY_PATH
, but for the Java virtual machine (JVM). Here, it points to the directory containing theliblttng-ust-jul-jni.so
native library.
With these specific paths, I assume that LTTng-UST is installed from source
into /usr/local
. If you installed LTTng-UST to a different location,
you need to update the paths accordingly.
This is a bit clunky, but it is just an example program. A conventional Java application could be packaged in a JAR file that advertises its dependencies explicitly. Also, in this example, the application instantiates the log handler itself. In more advanced use cases, as you will see below, handlers can be specified in a configuration file, so that they are transparent to the application.
By default, in the JVM, a ConsoleHandler
object is attached to the
root logger, which means that all log messages of level INFO
and above
are printed to the console. If you run the previous example, you should
get:
Jul 15, 2016 1:50:00 AM Hello main
INFO: Hello World, the answer is 42
Jul 15, 2016 1:50:01 AM Hello main
INFO: Another info event
Jul 15, 2016 1:50:01 AM Hello main
SEVERE: A severe event
This is nice, but not sufficient for us: we want the results in an LTTng
trace! Let's create a tracing session, enable LTTng events targetting
the JUL (--jul
) domain, and start it:
lttng create lttng enable-event --jul --all lttng start
Now run the example again:
java -classpath /usr/local/share/java/*:. -Djava.library.path=/usr/local/lib Hello
Finally, stop the tracing session, inspect the recorded events, and destroy the tracing session.
lttng stop lttng view lttng destroy
The lttng view
command shows the expected log records as LTTng
events:
[02:02:13.849817670] (+?.?????????) sonoshee lttng_jul:event: { cpu_id = 5 }, { msg = "Hello World, the answer is 42", logger_name = "Hello", class_name = "Hello", method_name = "main", long_millis = 1468562533846, int_loglevel = 800, int_threadid = 1 }
[02:02:13.884177233] (+0.034359563) sonoshee lttng_jul:event: { cpu_id = 5 }, { msg = "Another info event", logger_name = "Hello", class_name = "Hello", method_name = "main", long_millis = 1468562533884, int_loglevel = 800, int_threadid = 1 }
[02:02:13.885255957] (+0.001078724) sonoshee lttng_jul:event: { cpu_id = 5 }, { msg = "A severe event", logger_name = "Hello", class_name = "Hello", method_name = "main", long_millis = 1468562533885, int_loglevel = 1000, int_threadid = 1 }
Use a java.util.logging
configuration file
The previous example had the application define the log handler itself. In most real world use cases, the application would only take care of defining loggers, and handlers would be configured at run time by providing a JUL configuration file. Oracle's Using the JRE Logging Configuration File document is not particularly exhaustive, but should give a good idea of how it works. Plenty examples can be found on the Internet too.
For example, let's rerun the previous example, but this time with
a configuration file defining a FileHandler
handler. The
configuration file, let's call it logging.properties
, contains:
handlers = java.util.logging.FileHandler
java.util.logging.FileHandler.pattern = %h/program.log
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
The %h
token means your user's home directory.
Run the program, specifying the path to the configuration
file using the -Djava.util.logging.config.file
option:
java -classpath /usr/local/share/java/*:. \ -Djava.library.path=/usr/local/lib \ -Djava.util.logging.config.file=/path/to/logging.properties \ Hello
The generated log file contains the expected log events:
cat ~/program.log
Jul 15, 2016 2:20:14 AM Hello main
INFO: Hello World, the answer is 42
Jul 15, 2016 2:20:15 AM Hello main
INFO: Another info event
Jul 15, 2016 2:20:15 AM Hello main
SEVERE: A severe event
Note that both handlers and loggers have their own log levels, which
by default are set to INFO
. If you want to log lower-level messages,
like FINE
and below, you need to set the .level
property of both
the loggers and handlers you want to use.
Trace the Trace Compass application
With all this knowledge in hand, we can now record Trace Compass traces!
As a decent Java application, Trace Compass itself does not define any
log handlers, and expects that an external configuration file defines
them like we did previously. In fact, Trace Compass
disables all logging for the org.eclipse.tracecompass
namespace
unless the -Dorg.eclipse.tracecompass.logging=true
property is passed.
This default behaviour exists for performance reasons.
First, let us create the JUL configuration file that defines an
LTTng-UST Java agent log handler. Let's call
it logging-ust-tc.properties
:
org.eclipse.tracecompass.level = ALL
org.eclipse.tracecompass.handlers = org.lttng.ust.agent.jul.LttngLogHandler
org.eclipse.tracecompass.useParentHandlers = false
org.lttng.ust.agent.jul.LttngLogHandler.level = ALL
In large applications, it's not uncommon to have several components log
to java.util.logging
loggers. In our case, we are only interested in
the org.eclipse.tracecompass.*
events. This is why we attach the
handler only to that logger. We also use useParentHandlers = false
to
avoid leaking into the default ConsoleHandler
handler or whatever
other handlers may be defined manually by other components.
Before going any further, let's mention that Trace Compass, as an Eclipse application, makes use of the Equinox OSGi framework. OSGi, in general, is extremely paranoid about all class dependencies, and normally forbids loading classes or native libraries whose dependency has not been explicitly set by a bundle. This really goes against what we're trying to do here, which is to attach a log handler and make the application completely oblivious about it.
Fortunately, there is a workaround: instead of using the regular
-classpath
and -Djava.library.path
arguments, you can use
-Xbootclasspath
and -Dsun.boot.java.library.path
, which
forces those paths onto the boot class loader's configuration and
allows loading the LTTng-UST Java agent JAR and shared object files
into the program.
Be careful!-Xbootclasspath
and -Dsun.boot.library.path
are really low-level
JVM options, and should be used with care. Make sure you understand
what they do, and do not blindly load any class in there. We have
not found any side-effect of using those with the LTTng-UST
Java agent, but be warned.
Now, you just need to start Trace Compass by passing the following VM arguments:
-Xbootclasspath/a:/usr/local/share/java/lttng-ust-agent-jul.jar:/usr/local/share/java/lttng-ust-agent-common.jar
-Dsun.boot.library.path=/usr/local/lib
-Djava.util.logging.config.file=/path/to/logging-ust-tc.properties
-Dorg.eclipse.tracecompass.logging=true
They can be specified on the command line, after -vmargs
:
./tracecompass -vmargs \ -Xbootclasspath/a:/usr/local/share/java/lttng-ust-agent-jul.jar:/usr/local/share/java/lttng-ust-agent-common.jar \ -Dsun.boot.library.path=/usr/local/lib \ -Djava.util.logging.config.file=/path/to/logging-ust-tc.properties \ -Dorg.eclipse.tracecompass.logging=true
If you run Trace Compass from an Eclipse development environment, the VM arguments can be set in the VM arguments section of the run configuration:
Again, you might have to tweak the paths according to your installation.
Create an LTTng tracing session, enable all JUL events, and start tracing:
lttng create lttng enable-event --jul --all lttng start
Once you're done, stop tracing and inspect the events recorded so far:
lttng stop lttng view
Here's a sample of what I get:
Trace directory: /home/alexandre/lttng-traces/auto-20160715-032021
[03:27:16.369288260] (+?.?????????) sonoshee lttng_jul:event: { cpu_id = 6 }, { msg = "[TimeGraphView:LoadingTrace] trace=64-bit", logger_name = "org.eclipse.tracecompass.internal.analysis.os.linux.ui.views.controlflow.ControlFlowView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView", method_name = "loadTrace", long_millis = 1468567636368, int_loglevel = 800, int_threadid = 1 }
[03:27:16.369618897] (+0.000330637) sonoshee lttng_jul:event: { cpu_id = 6 }, { msg = "[TimeGraphView:RefreshRequested]", logger_name = "org.eclipse.tracecompass.internal.analysis.os.linux.ui.views.controlflow.ControlFlowView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView", method_name = "refresh", long_millis = 1468567636369, int_loglevel = 800, int_threadid = 1 }
[03:27:16.373662440] (+0.004043543) sonoshee lttng_jul:event: { cpu_id = 7 }, { msg = "[TimeGraphView:BuildThreadStart] trace=64-bit", logger_name = "org.eclipse.tracecompass.internal.analysis.os.linux.ui.views.controlflow.ControlFlowView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView$BuildThread", method_name = "run", long_millis = 1468567636373, int_loglevel = 800, int_threadid = 42 }
[03:27:16.375294339] (+0.001631899) sonoshee lttng_jul:event: { cpu_id = 7 }, { msg = "[TimeGraphView:BuildThreadEnd]", logger_name = "org.eclipse.tracecompass.internal.analysis.os.linux.ui.views.controlflow.ControlFlowView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView$BuildThread", method_name = "run", long_millis = 1468567636375, int_loglevel = 800, int_threadid = 42 }
[03:27:16.642176684] (+0.266882345) sonoshee lttng_jul:event: { cpu_id = 1 }, { msg = "[TimeGraphView:RefreshStart]", logger_name = "org.eclipse.tracecompass.internal.analysis.os.linux.ui.views.controlflow.ControlFlowView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView$6", method_name = "run", long_millis = 1468567636642, int_loglevel = 800, int_threadid = 1 }
[03:27:16.738085522] (+0.095908838) sonoshee lttng_jul:event: { cpu_id = 1 }, { msg = "[TimeGraphView:RefreshEnd]", logger_name = "org.eclipse.tracecompass.internal.analysis.os.linux.ui.views.controlflow.ControlFlowView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView$6", method_name = "run", long_millis = 1468567636737, int_loglevel = 800, int_threadid = 1 }
[03:29:18.167467846] (+121.429382324) sonoshee lttng_jul:event: { cpu_id = 5 }, { msg = "[StateSystem:FullQueryStart] ssid=org.eclipse.linuxtools.lttng2.ust.analysis.debuginfo, ts=1468529011041066742", logger_name = "org.eclipse.tracecompass.internal.statesystem.core.StateSystem", class_name = "org.eclipse.tracecompass.internal.statesystem.core.StateSystem", method_name = "queryFullState", long_millis = 1468567758167, int_loglevel = 800, int_threadid = 1 }
[03:29:18.169179684] (+0.001711838) sonoshee lttng_jul:event: { cpu_id = 5 }, { msg = "[StateSystem:FullQueryEnd]", logger_name = "org.eclipse.tracecompass.internal.statesystem.core.StateSystem", class_name = "org.eclipse.tracecompass.internal.statesystem.core.StateSystem", method_name = "queryFullState", long_millis = 1468567758169, int_loglevel = 800, int_threadid = 1 }
...
You could even open the Trace Compass trace into Trace Compass itself:
We've come full circle!
Trace specific components
You'll notice quite quickly that the StateSystem:*QueryStart
events,
for example, are very spammy, and if you are not interested in those, it
would be useful to stop tracing them and only trace the components you
are interested in. There are two ways to achieve this.
Use LTTng-UST filtering
In regular, native applications, you can enable specific LTTng events
based on their names. With the LTTng-UST Java agent,
however, all tracepoints share the same LTTng-UST event name, which is
lttng_jul:event
for the JUL handler. This means that the
lttng enable-event
command works a bit differently for the Java
domains: it actually filters on the logger name, as opposed to the
LTTng-UST event name itself.
For example, assuming you have started Trace Compass as we did previously, you can create a tracing session and enable JUL events as follows:
lttng create lttng enable-event --jul 'org.eclipse.tracecompass.tmf.ui.views.callstack*' lttng start # Do some operations involving the Callstack View lttng stop lttng view lttng destroy
The lttng view
command outputs something like this:
[04:11:16.747726586] (+?.?????????) sonoshee lttng_jul:event: { cpu_id = 7 }, { msg = "[TimeGraphView:LoadingTrace] trace=64-bit", logger_name = "org.eclipse.tracecompass.tmf.ui.views.callstack.CallStackView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView", method_name = "loadTrace", long_millis = 1468570276747, int_loglevel = 800, int_threadid = 1 }
[04:11:16.747782211] (+0.000055625) sonoshee lttng_jul:event: { cpu_id = 7 }, { msg = "[TimeGraphView:RefreshRequested]", logger_name = "org.eclipse.tracecompass.tmf.ui.views.callstack.CallStackView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView", method_name = "refresh", long_millis = 1468570276747, int_loglevel = 800, int_threadid = 1 }
[04:11:16.749022852] (+0.001240641) sonoshee lttng_jul:event: { cpu_id = 5 }, { msg = "[TimeGraphView:BuildThreadStart] trace=64-bit", logger_name = "org.eclipse.tracecompass.tmf.ui.views.callstack.CallStackView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView$BuildThread", method_name = "run", long_millis = 1468570276748, int_loglevel = 800, int_threadid = 91 }
[04:11:16.910564300] (+0.161541448) sonoshee lttng_jul:event: { cpu_id = 7 }, { msg = "[TimeGraphView:RefreshStart]", logger_name = "org.eclipse.tracecompass.tmf.ui.views.callstack.CallStackView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView$6", method_name = "run", long_millis = 1468570276910, int_loglevel = 800, int_threadid = 1 }
[04:11:16.935026917] (+0.024462617) sonoshee lttng_jul:event: { cpu_id = 7 }, { msg = "[TimeGraphView:RefreshEnd]", logger_name = "org.eclipse.tracecompass.tmf.ui.views.callstack.CallStackView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView$6", method_name = "run", long_millis = 1468570276934, int_loglevel = 800, int_threadid = 1 }
[04:11:16.935888918] (+0.000862001) sonoshee lttng_jul:event: { cpu_id = 1 }, { msg = "[TimeGraphView:ZoomThreadStart] start=-9223372036854775808, end=9223372036854775807", logger_name = "org.eclipse.tracecompass.tmf.ui.views.callstack.CallStackView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView$ZoomThread", method_name = "run", long_millis = 1468570276935, int_loglevel = 800, int_threadid = 94 }
[04:11:16.936177751] (+0.000288833) sonoshee lttng_jul:event: { cpu_id = 1 }, { msg = "[TimeGraphView:ZoomThreadGettingStates]", logger_name = "org.eclipse.tracecompass.tmf.ui.views.callstack.CallStackView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView$ZoomThreadByEntry", method_name = "doRun", long_millis = 1468570276936, int_loglevel = 700, int_threadid = 94 }
[04:11:16.936699963] (+0.000522212) sonoshee lttng_jul:event: { cpu_id = 1 }, { msg = "[TimeGraphView:ZoomThreadGettingLinks]", logger_name = "org.eclipse.tracecompass.tmf.ui.views.callstack.CallStackView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView$ZoomThreadByEntry", method_name = "doRun", long_millis = 1468570276936, int_loglevel = 700, int_threadid = 94 }
[04:11:16.937257688] (+0.000557725) sonoshee lttng_jul:event: { cpu_id = 1 }, { msg = "[TimeGraphView:ZoomThreadGettingMarkers]", logger_name = "org.eclipse.tracecompass.tmf.ui.views.callstack.CallStackView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView$ZoomThreadByEntry", method_name = "doRun", long_millis = 1468570276937, int_loglevel = 700, int_threadid = 94 }
[04:11:16.945664597] (+0.008406909) sonoshee lttng_jul:event: { cpu_id = 1 }, { msg = "[TimeGraphView:RedrawRequested]", logger_name = "org.eclipse.tracecompass.tmf.ui.views.callstack.CallStackView", class_name = "org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView", method_name = "redraw", long_millis = 1468570276945, int_loglevel = 800, int_threadid = 94 }
...
Notice that we only obtain events in which the logger_name
field is
within the org.eclipse.tracecompass.tmf.ui.views.callstack
namespace.
What happens here is that the LTTng-UST Java agent sends all log records through a Java Native Interface (JNI), and LTTng-UST filters out those that do not match the requirements of the tracing session.
Tweak the java.util.logging
configuration
Alternatively, it is also possible to filter events at the JUL level, by changing the contents of the configuration file.
If you want to
trace only the org.eclipse.tracecompass.tmf.ui.views.callstack
namespace, as we did previously, you can create a new JUL configuration
file with the following contents. Let's call it
logging-ust-tc-callstack.properties
:
org.eclipse.tracecompass.level = OFF
org.eclipse.tracecompass.tmf.ui.views.callstack.handlers = org.lttng.ust.agent.jul.LttngLogHandler
org.eclipse.tracecompass.tmf.ui.views.callstack.level = ALL
org.eclipse.tracecompass.tmf.ui.views.callstack.useParentHandlers = false
org.lttng.ust.agent.jul.LttngLogHandler.level = ALL
Note how we disable all org.eclipse.tracecompass
tracing, except for
the one component we are interested in.
Next, set up an LTTng tracing session as usual, this time using
lttng enable-event --jul --all
to enable all JUL events.
Restart Trace Compass, but make the
-Djava.util.logging.config.file
option point to the
logging-ust-tc-callstack.properties
file you just created.
The resulting trace should be similar to what we obtained previously,
with only events from the org.eclipse.tracecompass.tmf.ui.views.callstack
loggers being present.
Which approach should I use?
As we've seen, both filtering approaches accomplish the same goal.
The first one might be easier to set up and more flexible, since settings can be changed without having to restart the traced application. From a pure performance perspective, however, the second approach might be a little bit more efficient, since events that must not be recorded to the trace are not be sent through the JNI at all.
Setting the log level of unused loggers to OFF
is also a great help,
as it completely avoids the creation of the corresponding log record
objects, including potentially costly string concatenation operations.
Feel free to join the LTTng community if you have any question or hit any problem with the steps in this post.
Happy Java tracing!