пятница, 23 октября 2009 г.

"Hooking" into Java App Engine

"Hooking" into Java App Engine

Last week, Guido van Rossum gave a lightning talk on RPC instrumentation in App Engine. He was using api hooks to grab all sorts of useful information, then aggregated it in a web view. One of the questions that was asked after the talk was "how can this be done in Java?" I thought I'd give it a shot ;-). In this post, we are going to collect a very simple set of statistics. For each request, we are going to list all api calls that were made, with start and end time and whether they produced an error.

In order to get started, we need to find a way to tie into the RPC mechanism, similarly to the api proxy in python. As it turns out, there is also an ApiProxy class in Java, as described in the unit testing section of the documentation. In the documentation, there is a call

ApiProxy.setDelegate(new ApiProxyLocalImpl(new File(".")){});


where ApiProxyLocalImpl implements an interface com.google.apphosting.api.ApiProxy.Delegate. The delegate is not externally documented (as far as I know), but pulling up the class in Eclipse shows that there is a method makeSyncCall witn a bunch of arguments, where arguments 1 and 2 are strings. Let's try to to intercept that method call!

Since Delegate is not documented, we have no guarantees how it behaves, or if its contract overall will stay the same. Thus, I am reluctant to provide a full implementation of each method. Instead, let's use Dynamic Proxies:

package com.appenginefan.instrumentation;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;

import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.Delegate;

/**
* Catches calls to the ApiProxy and can perform
* measurements.
*/
public class Interceptor implements InvocationHandler {

private final Delegate<?> wrappedDelegate;
private final Delegate<?> wrapper;
private final ThreadLocal<List<String>> cache;

public Interceptor() {
this.wrappedDelegate = ApiProxy.getDelegate();
this.wrapper = (Delegate<?>) Proxy.newProxyInstance(
Interceptor.class.getClassLoader(),
new Class[]{Delegate.class},
this);
this.cache = new ThreadLocal<List<String>>();
}

/**
* Installs the interceptor in the ApiProxy.
*/
public void install() {
ApiProxy.setDelegate(wrapper);
}

/**
* Uninstalls the interceptor from the ApiProxy.
*/
public void uninstall() {
ApiProxy.setDelegate(wrappedDelegate);
}

/**
* Sets or removed a place where the interceptor can
* log method statistics for this call.
*/
public void setCache(List<String> localCache) {
cache.set(localCache);
}

@Override
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {

// Delegate to the wrapped proxy for most method calls
if (!method.getName().equals("makeSyncCall") ||
cache.get() == null) {
return method.invoke(wrappedDelegate, args);
}

// For sync-calls, let's collect some statistics
long startTime = System.currentTimeMillis();
String arg1 = String.valueOf(args[1]);
String arg2 = String.valueOf(args[2]);
Throwable errorInDelegate = null;
Object result = null;
try {
result = method.invoke(wrappedDelegate, args);
} catch (Throwable t) {
errorInDelegate = t;
}
long endTime = System.currentTimeMillis();

// Let's store the statistics somewhere
cache.get().add(String.format(
"%s.%s(), from %s, until %s, %s",
arg1, arg2, startTime, endTime,
(errorInDelegate == null) ?
"ok" : errorInDelegate.getMessage()));

// Return the proxied result, or rethrow exception
if (errorInDelegate != null) {
throw errorInDelegate;
} else {
return result;
}
}

}


This class will intercept only calls to makeSyncCall, and only if statistics collection is turned on for this particular request (by setting a statistics cache using setCache). It does not make many assumptions about the wrapped Delegate, execept that the method parameters for makeSyncCall contain at least three arguments.

Now, all we need is something to turn instrumentation on or off for specific requests. Assume we have the servlet we'd like to test mapped to the path /tst/, and this is only servlet we'd like to turn statistics on for. What we can do is create a servlet filter that we only enable for specific paths. In out web.xml, this might look something like this:

 <filter>
<filter-name>Stats</filter-name>
<filter-class>com.appenginefan.instrumentation.StatsCollector</filter-class>
</filter>
<filter-mapping>
<filter-name>Stats</filter-name>
<url-pattern>/tst/</url-pattern>
</filter-mapping>


Let's look at the implementation. The filter's job is to


  • Install the proxy upon startup

  • Uninstall the proxy during shutdown

  • Turn on stats and write the results to the log file.



Here is how such a class could look like:

package com.appenginefan.instrumentation;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class StatsCollector implements Filter {

private static final Logger LOG =
Logger.getLogger(StatsCollector.class.getName());
private Interceptor interceptor;

@Override
public void init(FilterConfig config)
throws ServletException {
interceptor = new Interceptor();
interceptor.install();
}

@Override
public void destroy() {
interceptor.uninstall();
}

@Override
public void doFilter(ServletRequest req,
ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
List<String> collectedData = new ArrayList<String>();
try {
interceptor.setCache(collectedData);
chain.doFilter(req, resp);
} finally {
interceptor.setCache(null);
}
write(collectedData);
}

protected void write(List<String> stats) {
LOG.info("API calls: " + stats);
}
}


In summary, while not explicitly mentioned in the meetup, it is already possible to collect useful information with Java App Engine. Naturally, the example above is much more primitive and not as feature-rich as what was shown during the presentation. However, it shows that implementing things such as hooks is just as feasible in Java as in the Python version.

Комментариев нет:

Отправить комментарий