Instrumenting custom methods

Instrumentation part of Zorka agent is called Zorka Spy.ZOrka features a fluent-style configuration API that uses similiar concepts to AOP (Aspect Oriented Programmin). It basically allows wrapping instrumented methods with

You can define spy configuration properties in fluent style using SpyDefinition object and then submit it to instrumentation engine. In order to be able to configure instrumentations, you need to understand structure of instrumentation engine and how events coming from instrumented methods are processed.

Zorka Spy will insert probes into certain points of instrumented methods. There are three kinds of points: entry points, return points and error points (when exception has been thrown). A probe can fetch some data, eg. current time, method argument, some class (method context) etc. All etched values are packed into a submission record (SpyRecord class) and processed by one of argument processing chains (ON_ENTER, ON_RETURN or ON_ERROR depending of probe type), then results of all probes from a method call are bound together and processed by ON_SUBMIT chain. All these operations are performed in method calling thread context (so these processing stages must be thread safe). After that, record is passed to ON_COLLECT chain which is guaranteed to be single threaded. Finally records are dispatched into collectors (which is also done in a single thread). Collectors can do various things with records: update statistics in some mbean, call some BSH function, log it to file etc. New collector implementations can be added on the fly - either in Java or as BeanShell scripts.

Spy definition example:

__mbsRegister() {
  process(record) {
    synchronized (super) {
        mbs = record.get("THIS").getServer();
        ccl = Thread.currentThread().getContextClassLoader();
        zorka.registerMbs("jboss", mbs, ccl);
    }
    return record;
  }
  return this;
} // __mbsRegister()


spy.add(spy.instance("JBOSS_MBS_REGISTER")
  .onReturn(spy.fetchArg("THIS", 0), (com.jitlogic.zorka.core.spy.SpyProcessor)__mbsRegister())
  .include(spy.byMethod(0, "org.jboss.naming.NamingService", "startService", null)));

This is part of Zorka configuration for JBoss 5.x. It intercepts instantiation of MBeanServerImpl (at the end of its constructor) and calls collect() function of jboss namespace (everything is declared in jboss namespace). SpyDefinition objects are immutable, so when tinkering with sdef multiple times, remember to assign result of last method call to some variable. Method spy.instance() returns empty (unconfigured) object of SpyDefinition type that can be further configured using methods described in next sections.

For more examples see Examples section above.

Choosing processing stages

Argument fetch and argument processing can be done in one of several points (see Diagram 1). Use the following functions to choose stage (or probe point):

sdef = sdef.onEnter(args...);
sdef = sdef.onReturn(args...);
sdef = sdef.onError(args...);
sdef = sdef.onSubmit(args...);

Fetching, processing and collecting data

Data to be fetched by probes can be defined using withArguments() method:

sdef = sdef.onEnter(arg1, arg2, ...);

Fetch probes, processors and collectors can be used as arguments (see spy library functions). Fetch probes produce data that are passed through processors to collectors as dictionary objects (Map<String,Object>). Data are accessed using string keys as in ordinary hash maps. Processors can get named values from map objects or write other values to passed maps. Processors can also filter out records. Collectors are just special kinds of processors that have some side effects. >Collector can update statistics, write to logs, send messages via syslog etc.

Additional notes:

  • when instrumenting constructors, be sure that this reference (if used) can be fetched only at constructor return - this is because at the beginning of a constructor this points to an uninitialized block of memory that does not represent any object, so instrumented class won’t load and will crash with class verifier error;

Looking for classes and methods to be instrumented

sdef = sdef.include(matcher1, matcher2, ...);

Using include() method administrator can add matching rules filtering which methods are to be instrumented. Methods matching any of passed matchers will be included. Matchers can be defined using spy.byXXX() functions.

Example: logging catalina requests

This example will instrument org.apache.catalina.core.StandardEngineValve.invoke() method and log its invocations in agent log.

spy.add(spy.instance("CATALINA_LOG_REQUESTS")
  .onEnter(spy.zorkaLog("INFO", "HTTP", "request occured."))
  .include(spy.byMethod("org.apache.catalina.core.StandardEngineValve", "invoke")));

The way it is done is to create empty spy configuration using spy.instance(), add some action on method entry using .onEnter() method and specify which methods that can be instrumented using .include() method. Finally, created spy configuration is passed to spy.add() function and thus registered in agent instrumentation engine. From now on every class matching specification in .include() will be processed by instrumentation engine and all its matching methods will be instrumented.

Slightly extended version of this script will also log URIs of incoming requests:

spy.add(spy.instance("CATALINA_LOG_REQUESTS")
  .onEnter(spy.fetchArg("REQ", 1), spy.zorkaLog("INFO", "HTTP", "${REQ.request.requestURI}"))
  .include(spy.byMethod("org.apache.catalina.core.StandardEngineValve", "invoke")));

Here we use spy.fetchArg() function to fetch first argument (which is request object), and then we use it inside spy.zorkaLog().