Write your own sirona javaagent extension


Apache Sirona java monitoring solution provides a javaagent. Its original purpose is to let Sirona itself instrument JVM or user classes to get metrics on execution time or execution stacks (what sirona calls path tracking). However API is open enough to be reused for totally different purposes. Let’s see how to use it to serve your own transversal needs.

To take a concrete example we will use Sirona javaagent to add a header to a HttpUrlConnection.

The header we will add will just say that the request origin is “us”, let’s call the header “sirona-origin” for the sake of the demo but real use cases would use a company specific naming with a value defined depending the JVM most probably.

Now we have the stage set let’s start the implementation!

First we need to implement a org.apache.sirona.javaagent.spi.InvocationListener. Let’s call it SironaOriginHttpHeaderAdder. The stub implementation is then:

package com.github.rmannibucau.sirona.javaagent.extension;

import org.apache.sirona.javaagent.AgentContext;
import org.apache.sirona.javaagent.spi.InvocationListener;

public class SironaOriginHttpHeaderAdder implements InvocationListener {
    @Override
    public boolean accept(final String key, final byte[] rawClassBuffer) {
        return false; // TODO
    }

    @Override
    public void before(final AgentContext context) {
// TODO
    }

    @Override
    public void after(final AgentContext context, final Object result, final Throwable error) {
// TODO
    }
}

Note: don’t forget by default Sirona relies on ServiceLoader so you need to create a META-INF/services/org.apache.sirona.javaagent.spi.InvocationListener containing the fully qualified name of your listener to get it registered.

Then the next implementation part is to know when we want to enter into the game: before the connection do the actual http connection, ie sun.net.www.protocol.http.HttpURLConnection.connect(). For that we just need to match this in InvocationListener#accept – of course several other pointcuts would work but this one is easy enough to worth it:

public class SironaOriginHttpHeaderAdder implements InvocationListener {
    @Override
    public boolean accept(final String key, final byte[] rawClassBuffer) {
        return key.equals("sun.net.www.protocol.http.HttpURLConnection.connect()");
    }

    // ...
}

Now the very hard part: actually add the header to the connection. In before(AgentContext) we can get the connection instance using AgentContext#getReference() so then it is just a standard java invocation:

public class SironaOriginHttpHeaderAdder implements InvocationListener {
    @Override
    public void before(final AgentContext context) {
        final HttpURLConnection connection = HttpURLConnection.class.cast(context.getReference());
        connection.setRequestProperty("sirona-origin", "us");
    }

    // ...
}

Of course in our case we don’t need the after(...) method to get any logic.

And here we are :). Now when our JVM will make any http connection using the JVM implementation we will get for free this sirona-origin header.

And now what? Are we done? We miss a test for our code isn’t it? Yeah testing javaagent is quite hard but Sirona needed to solve it for itself so with some efforts you can reuse Sirona tooling. InJvmTransformerRunner is quite nice but prevents to instrument JVM classes since it uses its own test classloader for the test. JavaAgentRunner creates a fork per test (@Test) so it is usable even for JVM classes. Of course as all advantage it comes with a small drawback: it is slower cause of the forks. However it is the solution we will use since it is the most reliable one for such low level hacking.

Note: I will use the SNAPSHOT since few fixes allow to make it super smooth – and blog friendly – but 0.3-incubating is usable with few more efforts.

The idea is to set a JUnit runner and just to ask this runner originally designed for sirona to use our freshly compiled classes at runtime – in the fork:

package com.github.rmannibucau.sirona.javaagent.extension;

import com.sun.net.httpserver.HttpServer;
import org.apache.sirona.javaagent.AgentArgs;
import org.apache.sirona.javaagent.JavaAgentRunner;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.URLConnection;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

@RunWith(JavaAgentRunner.class)
public class SironaOriginHttpHeaderAdderTest {
    private static HttpServer server;

    @BeforeClass
    public static void startHttpServer() throws IOException {
        server = HttpServer.create(new InetSocketAddress(1234), 0);
        server.start();
    }

    @AfterClass
    public static void stopServer() {
        server.stop(0);
    }

    @Test
    @AgentArgs(removeTargetClassesFromClasspath = false)
    public void addHeader() throws IOException {
        final URL url = new URL("http://localhost:" + server.getAddress().getPort());
        final URLConnection connection = url.openConnection();
        connection.setConnectTimeout(200);
        connection.setReadTimeout(100);
        try {
            connection.connect();
        } catch (final Exception e) {
            fail(e.getMessage());
        } finally {
            try {
                HttpURLConnection.class.cast(connection).disconnect();
            } catch (final Exception e) {
                // no-op
            }
        }


        assertEquals("us", connection.getRequestProperty("sirona-origin"));
    }
}

And here it is, the main point of this test are:

  • Using JavaAgentRunner JUnit runner
  • Using @AgentArgs to keep using our classes in the fork – sirona test setup is quite different from this

If you want to use Sirona 0.3-incubating release to write this test – the main part doesn’t change at all – you will need to:

  • copy the javaagent in a directory
  • set javaagent.jar.directory system property in surefire – or before the test execution – to this directory
  • (depending your test) add few dependencies – runner needs commons-lang3, commons-io but older versions can need openjpa

Few tips before ending this post on custom listeners:

  • if you implement a quite generic listener you can extend org.apache.sirona.javaagent.listener.ConfigurableListener to reuse sirona include/exclude default logic.
  • if you need after logic to match some before data you should pass the data through the AgentContext using its put method. The key is an integer which should be unique per listener (I would recommand you to use >= 1000 range for user listeners, the value itself has no importance but avoids to use the same as sirona internal listeners)
  • Code sample is on github: https://github.com/rmannibucau/sirona-javaagent-extension-sample
Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s