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
;
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:
35 * <filter-name>appstats</filter-name>
36 * <filter-class>com.google.appengine.tools.appstats.AppstatsFilter</filter-class>
39 * <filter-name>appstats</filter-name>
40 * <url-pattern>/*</url-pattern>
45 public class AppstatsFilter
implements Filter
{
50 static final String DEADLINE_MESSAGE
= "Deadline exceeded; cannot log app stats";
53 * Visible for testing.
55 static Logger log
= Logger
.getLogger(AppstatsFilter
.class.getName());
58 * Name of the HTTP header that will be included in the response.
60 static final String TRACE_HEADER_NAME
= "X-TraceUrl";
63 * The default values for the basePath init parameter.
65 private static final String DEFAULT_BASE_PATH
= "/appstats/";
68 * Threadsafe utility to manage appstats writes.
71 private Recording recording
;
74 * The delegate that was wrapped when the WRITER was created.
75 * Visible for testing.
77 static Delegate
<?
> delegate
;
80 * The base path where the AppStats dashboard can be found. This is provided
81 * as an init parameter.
83 private String basePath
;
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() {
99 AppstatsFilter(String basePath
, String logMessage
, Recording recording
) {
100 this.basePath
= basePath
;
101 this.logMessage
= logMessage
;
102 this.recording
= recording
;
106 public void destroy() {
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() {
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},
138 filters
.doFilter(request
, outerResponse
);
139 } catch (DeadlineExceededException e
) {
141 log
.warning(DEADLINE_MESSAGE
);
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");
160 return DEFAULT_BASE_PATH
;
162 return path
.endsWith("/") ? path
: path
+ "/";
166 @SuppressWarnings("unchecked")
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")) {
194 Class
<?
>[] interfaces
= original
.getClass().getInterfaces();
195 InvocationHandler handler
= new InvocationHandler(){
197 public Object
invoke(Object proxy
, Method method
, Object
[] args
) throws Throwable
{
198 Method wrapperMethod
= null;
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
);
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
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"));
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
);
257 throw new IllegalArgumentException(key
+ " must be a positive value");