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.
3 comments:
Google App Engine celebrates its first anniversary with a slew of announcements that increase the robustness of Google App Engine: flexible pricing, real cron support, ability to import/export data, a secure data connector, and the main feature: 100% Java App Engine.
heilpflanzen
All API calls in Java are handled by the class com.google.apphosting.api.ApiProxy. This class behaves similarly to the ApiProxyStubMap in the Python SDK, having makeSyncCall and makeAsyncCall functions that take care of invoking the relevant API calls.
accessoires
> This class behaves similarly to the ApiProxyStubMap in the Python SDK
Correct, and this is what this article uses. However, gathering all the information that the python version gathers is going to be a bit more tricky. See Nick's blog for an example:
http://blog.notdot.net/2009/11/App-Engine-Java-API-call-hooks
As you can see, the data arrives in the handler in binary format. One has to know what the proper classes are for decoding this. In python, the data still arrives in an object representation. There are a couple of other subtleties (like how to get to http request/response data) that are a bit more complex in Java. But beyond that, yes, the mechanism is very similar to python.
Post a Comment