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
;
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:
37 <filter-name>appstats</filter-name>
38 <filter-class>com.google.appengine.tools.appstats.AppstatsFilter</filter-class>
41 <filter-name>appstats</filter-name>
42 <url-pattern>/*</url-pattern>
47 public class AppstatsFilter
implements Filter
{
52 static final String DEADLINE_MESSAGE
= "Deadline exceeded; cannot log app stats";
55 * Visible for testing.
57 static Logger log
= Logger
.getLogger(AppstatsFilter
.class.getName());
60 * Name of the HTTP header that will be included in the response.
62 static final String TRACE_HEADER_NAME
= "X-TraceUrl";
65 * The default values for the basePath init parameter.
67 private static final String DEFAULT_BASE_PATH
= "/appstats/";
70 * Static to enforce "singleton" even if multiple instances of the filter are created.
73 static Recorder
.RecordWriter writer
= null;
76 * The delegate that was wrapped when the WRITER was created.
77 * Visible for testing.
79 static Delegate
<?
> delegate
;
82 * The base path where the AppStats dashboard can be found. This is provided
83 * as an init parameter.
85 private String basePath
;
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() {
101 AppstatsFilter(String basePath
, String logMessage
) {
102 this.basePath
= basePath
;
103 this.logMessage
= logMessage
;
107 public void destroy() {
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(){
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
);
136 filters
.doFilter(request
, outerResponse
);
137 } catch (DeadlineExceededException e
) {
139 log
.warning(DEADLINE_MESSAGE
);
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");
157 return DEFAULT_BASE_PATH
;
159 return path
.endsWith("/") ? path
: path
+ "/";
163 @SuppressWarnings("unchecked")
165 public synchronized void init(FilterConfig config
) {
166 if (writer
== null) {
167 Recorder
.RecordWriter newWriter
=
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
));
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")) {
195 Class
<?
>[] interfaces
= original
.getClass().getInterfaces();
196 InvocationHandler handler
= new InvocationHandler(){
198 public Object
invoke(Object proxy
, Method method
, Object
[] args
) throws Throwable
{
199 Method wrapperMethod
= null;
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
{
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) {
235 recorder
.setPayloadRenderer(
236 (PayloadRenderer
) Class
.forName(config
.getInitParameter("payloadRenderer"))
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
);
266 throw new IllegalArgumentException(key
+ " must be a positive value");