1.9.30 sync.
[gae.git] / java / src / main / com / google / appengine / tools / appstats / AppstatsFilter.java
blob83535e5a4ef500867fabc3eba08a1bdb9e9f931f
1 // Copyright 2009 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools.appstats;
5 import com.google.apphosting.api.ApiProxy;
6 import com.google.apphosting.api.ApiProxy.Delegate;
7 import com.google.apphosting.api.ApiProxy.Environment;
8 import com.google.apphosting.api.DeadlineExceededException;
9 import com.google.common.base.Preconditions;
11 import java.io.IOException;
12 import java.lang.reflect.InvocationHandler;
13 import java.lang.reflect.InvocationTargetException;
14 import java.lang.reflect.Method;
15 import java.lang.reflect.Proxy;
16 import java.util.logging.Logger;
18 import javax.servlet.Filter;
19 import javax.servlet.FilterChain;
20 import javax.servlet.FilterConfig;
21 import javax.servlet.ServletException;
22 import javax.servlet.ServletRequest;
23 import javax.servlet.ServletResponse;
24 import javax.servlet.http.HttpServletRequest;
25 import javax.servlet.http.HttpServletResponse;
27 /**
28 * A servlet filter that will time RPCs going to the server and collect statistics.
29 * Add this filter to any servlet that you would like to monitor.
31 * The simplest way to configure an application for appstats collection is this:
33 * <p><pre>{@code
34 * <filter>
35 * <filter-name>appstats</filter-name>
36 * <filter-class>com.google.appengine.tools.appstats.AppstatsFilter</filter-class>
37 * </filter>
38 * <filter-mapping>
39 * <filter-name>appstats</filter-name>
40 * <url-pattern>/*</url-pattern>
41 * </filter-mapping>
42 * }</pre>
45 public class AppstatsFilter implements Filter {
47 /**
48 * Visible for testing
50 static final String DEADLINE_MESSAGE = "Deadline exceeded; cannot log app stats";
52 /**
53 * Visible for testing.
55 static Logger log = Logger.getLogger(AppstatsFilter.class.getName());
57 /**
58 * Name of the HTTP header that will be included in the response.
60 static final String TRACE_HEADER_NAME = "X-TraceUrl";
62 /**
63 * The default values for the basePath init parameter.
65 private static final String DEFAULT_BASE_PATH = "/appstats/";
67 /**
68 * Threadsafe utility to manage appstats writes.
71 private Recording recording;
73 /**
74 * The delegate that was wrapped when the WRITER was created.
75 * Visible for testing.
77 static Delegate<?> delegate;
79 /**
80 * The base path where the AppStats dashboard can be found. This is provided
81 * as an init parameter.
83 private String basePath;
85 /**
86 * A log messsage that may be used to store a link back to app stats.
87 * The ID is referred to as {ID}.
89 private String logMessage;
91 private Recorder recorder;
93 public AppstatsFilter() {
96 /**
97 * Visible for testing
99 AppstatsFilter(String basePath, String logMessage, Recording recording) {
100 this.basePath = basePath;
101 this.logMessage = logMessage;
102 this.recording = recording;
105 @Override
106 public void destroy() {
109 @Override
110 public void doFilter(ServletRequest request, ServletResponse response, FilterChain filters)
111 throws IOException, ServletException {
112 Preconditions.checkNotNull(recording, "recording shouldn't be null");
113 Environment environment = getCurrentEnvironment();
114 environment.getAttributes().put(Recording.RECORDING_KEY, recording);
115 Long id = recording.begin(delegate, environment, (HttpServletRequest) request);
116 final HttpServletResponse innerResponse = (HttpServletResponse) response;
117 final int[] responseCode = {0};
119 innerResponse.addHeader(TRACE_HEADER_NAME,
120 basePath + "details?time=" + id + "&type=json");
122 InvocationHandler invocationHandler = new InvocationHandler() {
123 @Override
124 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
125 if (method.getName().equals("sendError") || method.getName().equals("setStatus")) {
126 responseCode[0] = ((int) args[0]);
127 } else if (method.getName().equals("sendRedirect")) {
128 responseCode[0] = HttpServletResponse.SC_TEMPORARY_REDIRECT;
130 return call(method, innerResponse, args);
133 final HttpServletResponse outerResponse = (HttpServletResponse) Proxy.newProxyInstance(
134 AppstatsFilter.class.getClassLoader(),
135 new Class<?>[] {HttpServletResponse.class},
136 invocationHandler);
137 try {
138 filters.doFilter(request, outerResponse);
139 } catch (DeadlineExceededException e) {
140 id = null;
141 log.warning(DEADLINE_MESSAGE);
142 throw e;
143 } finally {
144 if (id != null) {
145 if (recorder != null) {
146 recorder.processAsyncRpcs(environment);
148 recording.finishCustomRecordings();
149 boolean didCommit = recording.commit(delegate, environment, responseCode[0]);
150 if (logMessage != null && didCommit) {
151 log.info(logMessage.replace("{ID}", "" + id));
157 private static String getAppStatsPathFromConfig(FilterConfig config) {
158 String path = config.getInitParameter("basePath");
159 if (path == null) {
160 return DEFAULT_BASE_PATH;
161 } else {
162 return path.endsWith("/") ? path : path + "/";
166 @SuppressWarnings("unchecked")
167 @Override
168 public synchronized void init(FilterConfig config) {
169 if (recording == null) {
170 AppstatsSettings settings = initializeSettings(config);
171 recording = new Recording(settings);
172 Recorder.RecordWriter newWriter = recording.getWriter();
173 delegate = ApiProxy.getDelegate();
175 recorder = new Recorder(delegate, newWriter, settings);
176 ApiProxy.setDelegate(wrapPartially(delegate, recorder));
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 static <S, T extends S> S wrapPartially(final S original, final T wrapper) {
190 if (!original.getClass().getName().contains("Local")) {
191 return wrapper;
194 Class<?>[] interfaces = original.getClass().getInterfaces();
195 InvocationHandler handler = new InvocationHandler(){
196 @Override
197 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
198 Method wrapperMethod = null;
199 try {
200 wrapperMethod = wrapper.getClass().getMethod(
201 method.getName(), method.getParameterTypes());
202 } catch (NoSuchMethodException e) {
203 return call(method, original, args);
205 return call(wrapperMethod, wrapper, args);
207 @SuppressWarnings("unchecked")
208 S ret = (S) Proxy.newProxyInstance(original.getClass().getClassLoader(), interfaces, handler);
209 return ret;
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 getCurrentEnvironment() {
227 return ApiProxy.getCurrentEnvironment();
230 static AppstatsSettings initializeSettings(FilterConfig config) {
231 Preconditions.checkNotNull(config, "FilterConfig can not be null.");
232 AppstatsSettings settings = AppstatsSettings.withDefault();
233 settings.setMaxLinesOfStackTrace(
234 getPositiveInt(config, "maxLinesOfStackTrace", Integer.MAX_VALUE));
235 if (config.getInitParameter("payloadRenderer") != null) {
236 settings.setPayloadRenderer(config.getInitParameter("payloadRenderer"));
238 if (config.getInitParameter("onPendingAsyncCall") != null) {
239 settings.setUnprocessedFutureStrategy(config.getInitParameter("onPendingAsyncCall"));
241 if (config.getInitParameter("calculateRpcCosts") != null) {
242 settings.setCalculateRpcCosts(config.getInitParameter("calculateRpcCosts"));
244 if (config.getInitParameter("datastoreDetails") != null) {
245 settings.setDatastoreDetails(config.getInitParameter("datastoreDetails"));
248 return settings;
251 static int getPositiveInt(FilterConfig config, String key, int defaultValue) {
252 int result = defaultValue;
253 String stringValue = config.getInitParameter(key);
254 if (stringValue != null) {
255 result = Integer.parseInt(stringValue);
256 if (result <= 0) {
257 throw new IllegalArgumentException(key + " must be a positive value");
260 return result;