Skip to content


If you have library code that doesn't pass around ILoggerFactory and doesn't let you add information to logging, then you can get around this by instrumenting the code with Byte Buddy. Using Byte Buddy, you can do fun things like override Security.setSystemManager with your own implementation, so using Byte Buddy to decorate code with enter and exit logging statements is relatively straightforward.

Instrumentation is configuration driven and simple. Instead of debugging using printf statements and recompiling or stepping through a debugger, you can just add lines to a config file.

I like this approach better than the annotation or aspect-oriented programming approaches, because it is completely transparent to the code and gives roughly the same performance as inline code, adding 130 ns/op by calling class.getMethod.

A major advantage of instrumentation is that because it logs throwing exceptions in instrumented code, you can log exceptions that would be swallowed by the caller. For example, imagine that a library has the following method:

public class Foo {
    public void throwException() throws Exception {
        throw new PlumException("I am sweet and cold");

    public void swallowException() {
        try {
        } catch (Exception e) {
            // forgive me, the exception was delicious

By instrumenting the throwException method, you can see the logged exception at runtime when swallowException is called.

See Application Logging in Java: Tracing 3rd Party Code and Hierarchical Instrumented Tracing with Logback for more details.


You'll need to install logback-bytebuddy and logback-tracing, and provide a byte-buddy implementation.

implementation group: 'com.tersesystems.logback', name: 'logback-classic', version: '0.16.2'
implementation group: 'com.tersesystems.logback', name: 'logback-bytebuddy', version: '0.16.2'
implementation group: 'com.tersesystems.logback', name: 'logback-tracing', version: '0.16.2'

implementation group: 'net.bytebuddy', name: 'byte-buddy', version: '1.11.0'

There are two ways you can install instrumentation -- you can do it using an agent, or you can do it manually.

NOTE: Because Byte Buddy must inspect each class on JVM initialization, it will have a (generally small) impact on the start up time of your application.

Agent Installation

Using the agent is generally easier (less code) and more powerful (can change JDK classes), but it does require some explicit command line options.

First, you set the java agent, either directly on the command line:

java \
  -javaagent:path/to/logback-bytebuddy-x.x.x.jar=debug \
  -Dterse.logback.configurationFile=conf/logback.conf \
  -Dlogback.configurationFile=conf/logback-test.xml \

or by using the JAVA_TOOLS_OPTIONS environment variable.


Generally you'll be setting up these options in a build system. There are example projects in Gradle and sbt set up with agent-based instrumentation at

Manual Installation

You also have the option of installing the agent manually.

The in process instrumentation is done with com.tersesystems.logback.bytebuddy.LoggingInstrumentationByteBuddyBuilder, which takes in some configuration and then installs itself on the byte buddy agent.

new LoggingInstrumentationByteBuddyBuilder()


There are two parts to seeing tracing logs with instrumentation -- indicating the classes and methods you want instrumented, and then setting those loggers to TRACE.

Setting Instrumented Classes and Methods

The instrumentation is configured using HOCON in a logback.conf file in src/main/resources.

Settings are under the logback.bytebuddy section. The tracing section contains a mapping of class names and methods, or the wildcard "*" to indicate all methods.

logback.bytebuddy {
  service-name = "my-service"

  tracing {    
    "fully.qualified.class.Name" = ["method1", "method2"]
    "play.api.mvc.ActionBuilder" = ["*"]

NOTE: There are some limitations to what you can trace. You can only instrument JDK classes when using the agent, and you cannot instrument native methods like java.lang.System.currentTimeMillis() for example.

Setting Loggers to TRACE

Because instrumentation inserts logger.trace calls into the code, you must enable logging at TRACE level for those loggers to see output. Setting the level from logback.xml works fine:

    <!-- ... -->
    <logger name="fully.qualified.class.Name" level="TRACE"/>
    <logger name="play.api.mvc.ActionBuilder" level="TRACE"/>
    <!-- ... -->

If you are using the Config module, you can also do this from logback.conf:

levels {
  fully.qualified.class.Name = TRACE
  play.api.mvc.ActionBuilder = TRACE

Or you can use ChangeLogLevel at run time.


Instrumentation is a tool that can be hard to explain, so here's some use cases showing how you can quickly instrument your code.

Also don't forget the example projects at

Instrumenting java.lang.Thread

Assuming an agent based instrumentation, in logback.conf:

levels {
  java.lang.Thread = TRACE

logback.bytebuddy {
  service-name = "some-service"
  tracing {
    "java.lang.Thread" = [

and the code as follows:

public class PreloadedInstrumentationExample {
    public static void main(String[] args) throws Exception {
        Thread thread = Thread.currentThread();;


[Byte Buddy] DISCOVERY java.lang.Thread [null, null, loaded=true]
[Byte Buddy] TRANSFORM java.lang.Thread [null, null, loaded=true]
[Byte Buddy] COMPLETE java.lang.Thread [null, null, loaded=true]
92    TRACE java.lang.Thread - entering: with arguments=[]
93    TRACE java.lang.Thread - exiting: with arguments=[] => returnType=void


This is especially helpful when you're trying to debug SSL issues:

levels { = TRACE = TRACE

logback.bytebuddy {
  service-name = "some-service"
  tracing {  
    "" = ["*"]

will result in:

FcJ3XfsdKnM6O0Qbm7EAAA 12:31:55.498 [TRACE] j.n.s.SSLContext -  entering: with arguments=[TLS] from source
FcJ3XfsdKng6O0Qbm7EAAA 12:31:55.503 [TRACE] j.n.s.SSLContext -  exiting: with arguments=[TLS] => from source
FcJ3XfsdKng6O0Qbm7EAAB 12:31:55.504 [TRACE] j.n.s.SSLContext -  entering:[;,[;, with arguments=[[org.postgresql.ssl.LazyKeyManager@27a97e08], [org.postgresql.ssl.NonValidatingFactory$NonValidatingTM@5918c260], null] from source
FcJ3XfsdKnk6O0Qbm7EAAA 12:31:55.504 [TRACE] j.n.s.SSLContext -  exiting:[;,[;, with arguments=[[org.postgresql.ssl.LazyKeyManager@27a97e08], [org.postgresql.ssl.NonValidatingFactory$NonValidatingTM@5918c260], null] => returnType=void from source

Be warned that JSSE can be extremely verbose in its toString output.

Instrumenting ClassCalledByAgent

If you are already developing an agent, or want finer grained control over Byte Buddy, you can create the agent in process and inspect how Byte Buddy works. This is an advanced use case, but it's useful to get familiar.

With the following code:

public class ClassCalledByAgent {
    public void printStatement() {
        System.out.println("I am a simple println method with no logging");

    public void printArgument(String arg) {
        System.out.println("I am a simple println, printing " + arg);

    public void throwException(String arg) {
        throw new RuntimeException("I'm a squirrel!");

And the following configuration in logback.conf:

logback.bytebuddy {
  service-name = "example-service"
  tracing {
    "com.tersesystems.logback.bytebuddy.ClassCalledByAgent" = [

and have com.tersesystems.logback.bytebuddy.ClassCalledByAgent logging level set to TRACE in logback.xml.

We can start up the agent, add in the builder and run through the methods:

public class InProcessInstrumentationExample {

    public static AgentBuilder.Listener createDebugListener(List<String> classNames) {
        return new AgentBuilder.Listener.Filtering(

    public static void main(String[] args) throws Exception {
        // Helps if you install the byte buddy agents before anything else at all happens...

        Logger logger = LoggerFactory.getLogger(InProcessInstrumentationExample.class);
        SystemFlow.setLoggerResolver(new FixedLoggerResolver(logger));

        Config config = LoggingInstrumentationAdvice.generateConfig(ClassLoader.getSystemClassLoader(), false);
        LoggingInstrumentationAdviceConfig adviceConfig = LoggingInstrumentationAdvice.generateAdviceConfig(config);

        // The debugging listener shows what classes are being picked up by the instrumentation
        Listener debugListener = createDebugListener(adviceConfig.classNames());
        new LoggingInstrumentationByteBuddyBuilder()

        // No code change necessary here, you can wrap completely in the agent...
        ClassCalledByAgent classCalledByAgent = new ClassCalledByAgent();

        try {
            classCalledByAgent.throwException("hello world");
        } catch (Exception e) {
            // I am too lazy to catch this exception.  I hope someone does it for me.

And get the following:

[Byte Buddy] DISCOVERY com.tersesystems.logback.bytebuddy.ClassCalledByAgent [sun.misc.Launcher$AppClassLoader@75b84c92, null, loaded=true]
[Byte Buddy] TRANSFORM com.tersesystems.logback.bytebuddy.ClassCalledByAgent [sun.misc.Launcher$AppClassLoader@75b84c92, null, loaded=true]
[Byte Buddy] COMPLETE com.tersesystems.logback.bytebuddy.ClassCalledByAgent [sun.misc.Launcher$AppClassLoader@75b84c92, null, loaded=true]
524   TRACE c.t.l.b.InProcessInstrumentationExample - entering: com.tersesystems.logback.bytebuddy.ClassCalledByAgent.printStatement() with arguments=[] from source
I am a simple println method with no logging
529   TRACE c.t.l.b.InProcessInstrumentationExample - exiting: com.tersesystems.logback.bytebuddy.ClassCalledByAgent.printStatement() with arguments=[] => returnType=void from source
529   TRACE c.t.l.b.InProcessInstrumentationExample - entering: com.tersesystems.logback.bytebuddy.ClassCalledByAgent.printArgument(java.lang.String) with arguments=[42] from source
I am a simple println, printing 42
529   TRACE c.t.l.b.InProcessInstrumentationExample - exiting: com.tersesystems.logback.bytebuddy.ClassCalledByAgent.printArgument(java.lang.String) with arguments=[42] => returnType=void from source
529   TRACE c.t.l.b.InProcessInstrumentationExample - entering: com.tersesystems.logback.bytebuddy.ClassCalledByAgent.throwException(java.lang.String) with arguments=[hello world] from source
532   ERROR c.t.l.b.InProcessInstrumentationExample - throwing: com.tersesystems.logback.bytebuddy.ClassCalledByAgent.throwException(java.lang.String) with arguments=[hello world] ! thrown=java.lang.RuntimeException: I'm a squirrel!
java.lang.RuntimeException: I'm a squirrel!
    at com.tersesystems.logback.bytebuddy.ClassCalledByAgent.throwException(
    at com.tersesystems.logback.bytebuddy.InProcessInstrumentationExample.main(

The [Byte Buddy] statements up top are caused by the debug listener, and let you know that Byte Buddy has successfully instrumented the class. Note also that there is no runtime overhead in pulling line numbers or source files into the enter/exit methods, as these are pulled directly from bytecode and do not involve fillInStackTrace.