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 itsput
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