Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / appstats / AppstatsFilter.java
blob68491a4bccb918cde2b154d443e0272dbf11e607
1 // Copyright 2009 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools.appstats;
5 import com.google.appengine.api.memcache.MemcacheServiceFactory;
6 import com.google.appengine.tools.appstats.Recorder.UnprocessedFutureStrategy;
7 import com.google.apphosting.api.ApiProxy;
8 import com.google.apphosting.api.DeadlineExceededException;
9 import com.google.apphosting.api.ApiProxy.Delegate;
10 import com.google.apphosting.api.ApiProxy.Environment;
12 import java.io.IOException;
13 import java.lang.reflect.InvocationHandler;
14 import java.lang.reflect.InvocationTargetException;
15 import java.lang.reflect.Method;
16 import java.lang.reflect.Proxy;
17 import java.util.logging.Logger;
19 import javax.servlet.Filter;
20 import javax.servlet.FilterChain;
21 import javax.servlet.FilterConfig;
22 import javax.servlet.ServletException;
23 import javax.servlet.ServletRequest;
24 import javax.servlet.ServletResponse;
25 import javax.servlet.http.HttpServletRequest;
26 import javax.servlet.http.HttpServletResponse;
28 /**
29 * A servlet filter that will time RPCs going to the server and collect statistics.
30 * Add this filter to any servlet that you would like to monitor.
32 * The simplest way to configure an application for appstats collection is this:
35 * <pre>
36 <filter>
37 <filter-name>appstats</filter-name>
38 <filter-class>com.google.appengine.tools.appstats.AppstatsFilter</filter-class>
39 </filter>
40 <filter-mapping>
41 <filter-name>appstats</filter-name>
42 <url-pattern>/*</url-pattern>
43 </filter-mapping>
44 </pre>
47 public class AppstatsFilter implements Filter {
49 /**
50 * Visible for testing
52 static final String DEADLINE_MESSAGE = "Deadline exceeded; cannot log app stats";
54 /**
55 * Visible for testing.
57 static Logger log = Logger.getLogger(AppstatsFilter.class.getName());
59 /**
60 * Name of the HTTP header that will be included in the response.
62 static final String TRACE_HEADER_NAME = "X-TraceUrl";
64 /**
65 * The default values for the basePath init parameter.
67 private static final String DEFAULT_BASE_PATH = "/appstats/";
69 /**
70 * Static to enforce "singleton" even if multiple instances of the filter are created.
71 * Visible for testing
73 static Recorder.RecordWriter writer = null;
75 /**
76 * The delegate that was wrapped when the WRITER was created.
77 * Visible for testing.
79 static Delegate<?> delegate;
81 /**
82 * The base path where the AppStats dashboard can be found. This is provided
83 * as an init parameter.
85 private String basePath;
87 /**
88 * A log messsage that may be used to store a link back to app stats.
89 * The ID is referred to as {ID}.
91 private String logMessage;
93 private Recorder recorder;
95 public AppstatsFilter() {
98 /**
99 * Visible for testing
101 AppstatsFilter(String basePath, String logMessage) {
102 this.basePath = basePath;
103 this.logMessage = logMessage;
106 @Override
107 public void destroy() {
110 @Override
111 public void doFilter(ServletRequest request, ServletResponse response, FilterChain filters)
112 throws IOException, ServletException {
113 Environment environment = getCurrentEvnvironment();
114 Long id = writer.begin(delegate, environment, (HttpServletRequest) request);
115 final HttpServletResponse innerResponse = (HttpServletResponse) response;
116 final Integer[] responseCode = {null};
118 innerResponse.addHeader(TRACE_HEADER_NAME,
119 basePath + "details?time=" + id + "&type=json");
121 final HttpServletResponse outerResponse = (HttpServletResponse) Proxy.newProxyInstance(
122 AppstatsFilter.class.getClassLoader(),
123 new Class[]{HttpServletResponse.class},
124 new InvocationHandler(){
125 @Override
126 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
127 if (method.getName().equals("sendError") || method.getName().equals("setStatus")) {
128 responseCode[0] = ((Number) args[0]).intValue();
129 } else if (method.getName().equals("sendRedirect")) {
130 responseCode[0] = HttpServletResponse.SC_TEMPORARY_REDIRECT;
132 return call(method, innerResponse, args);
135 try {
136 filters.doFilter(request, outerResponse);
137 } catch (DeadlineExceededException e) {
138 id = null;
139 log.warning(DEADLINE_MESSAGE);
140 throw e;
141 } finally {
142 if (id != null) {
143 if (recorder != null) {
144 recorder.processAsyncRpcs(environment);
146 boolean didCommit = writer.commit(delegate, environment, responseCode[0]);
147 if (logMessage != null && didCommit) {
148 log.info(logMessage.replace("{ID}", "" + id));
154 private static String getAppStatsPathFromConfig(FilterConfig config) {
155 String path = config.getInitParameter("basePath");
156 if (path == null) {
157 return DEFAULT_BASE_PATH;
158 } else {
159 return path.endsWith("/") ? path : path + "/";
163 @SuppressWarnings("unchecked")
164 @Override
165 public synchronized void init(FilterConfig config) {
166 if (writer == null) {
167 Recorder.RecordWriter newWriter =
168 new MemcacheWriter(
169 new Recorder.Clock(),
170 MemcacheServiceFactory.getMemcacheService(MemcacheWriter.STATS_NAMESPACE));
171 delegate = ApiProxy.getDelegate();
173 recorder = new Recorder(delegate, newWriter);
174 configureRecorder(config, recorder);
175 ApiProxy.setDelegate(wrapPartially(delegate, recorder));
176 writer = newWriter;
178 basePath = getAppStatsPathFromConfig(config);
179 logMessage = config.getInitParameter("logMessage");
183 * Create a proxy that implements all the interfaces that the original
184 * implements. Whenever a method is called that the wrapper supports,
185 * the wrapper will be called. Otherwise, the method will be invoked on
186 * the original object.
188 @SuppressWarnings("unchecked")
189 static <S, T extends S> S wrapPartially(final S original, final T wrapper) {
191 if (!original.getClass().getName().contains("Local")) {
192 return wrapper;
195 Class<?>[] interfaces = original.getClass().getInterfaces();
196 InvocationHandler handler = new InvocationHandler(){
197 @Override
198 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
199 Method wrapperMethod = null;
200 try {
201 wrapperMethod = wrapper.getClass().getMethod(
202 method.getName(), method.getParameterTypes());
203 } catch (NoSuchMethodException e) {
204 return call(method, original, args);
206 return call(wrapperMethod, wrapper, args);
208 return (S) Proxy.newProxyInstance(
209 original.getClass().getClassLoader(), interfaces, handler);
213 * Invoke a method and unwrap exceptions the invoked method may throw.
215 private static Object call(Method m, Object o, Object[] args) throws Throwable {
216 try {
217 return m.invoke(o, args);
218 } catch (InvocationTargetException e) {
219 throw e.getTargetException();
224 * Visible for testing
226 Environment getCurrentEvnvironment() {
227 return ApiProxy.getCurrentEnvironment();
230 static void configureRecorder(FilterConfig config, Recorder recorder) {
231 recorder.setMaxLinesOfStackTrace(getPositiveInt(
232 config, "maxLinesOfStackTrace", Integer.MAX_VALUE));
233 if (config.getInitParameter("payloadRenderer") != null) {
234 try {
235 recorder.setPayloadRenderer(
236 (PayloadRenderer) Class.forName(config.getInitParameter("payloadRenderer"))
237 .newInstance());
238 } catch (InstantiationException e) {
239 throw new IllegalArgumentException("Cannot instantiate payloadRenderer", e);
240 } catch (IllegalAccessException e) {
241 throw new IllegalArgumentException("Cannot instantiate payloadRenderer", e);
242 } catch (ClassNotFoundException e) {
243 throw new IllegalArgumentException("Cannot instantiate payloadRenderer", e);
244 } catch (ClassCastException e) {
245 throw new IllegalArgumentException("Cannot instantiate payloadRenderer", e);
248 if (config.getInitParameter("onPendingAsyncCall") != null) {
249 recorder.setUnprocessedFutureStrategy(
250 UnprocessedFutureStrategy.valueOf(config.getInitParameter("onPendingAsyncCall")));
252 if (config.getInitParameter("calculateRpcCosts") != null) {
253 recorder.setCalculateRpcCosts(Boolean.valueOf(config.getInitParameter("calculateRpcCosts")));
255 if (config.getInitParameter("datastoreDetails") != null) {
256 recorder.setDatastoreDetails(Boolean.valueOf(config.getInitParameter("datastoreDetails")));
260 static int getPositiveInt(FilterConfig config, String key, int defaultValue) {
261 int result = defaultValue;
262 String stringValue = config.getInitParameter(key);
263 if (stringValue != null) {
264 result = Integer.parseInt(stringValue);
265 if (result <= 0) {
266 throw new IllegalArgumentException(key + " must be a positive value");
269 return result;