001 package org.javasimon.javaee;
002
003 import org.javasimon.Manager;
004 import org.javasimon.SimonManager;
005 import org.javasimon.Split;
006 import org.javasimon.callback.CallbackSkeleton;
007 import org.javasimon.clock.ClockUtils;
008 import org.javasimon.javaee.reqreporter.RequestReporter;
009 import org.javasimon.source.DisabledMonitorSource;
010 import org.javasimon.source.StopwatchSource;
011 import org.javasimon.utils.Replacer;
012 import org.javasimon.utils.SimonUtils;
013 import org.javasimon.utils.bean.SimonBeanUtils;
014 import org.javasimon.utils.bean.ToEnumConverter;
015
016 import javax.servlet.*;
017 import javax.servlet.http.HttpServletRequest;
018 import javax.servlet.http.HttpServletResponse;
019 import java.io.IOException;
020 import java.util.ArrayList;
021 import java.util.List;
022
023 /**
024 * Simon Servlet filter measuring HTTP request execution times. Non-HTTP usages are not supported.
025 * Filter provides these functions:
026 * <ul>
027 * <li>measures all requests and creates tree of Simons with names derived from URLs</li>
028 * <li>checks if the request is not longer then a specified threshold and logs warning</li>
029 * <li>provides basic "console" function if config parameter {@link #INIT_PARAM_SIMON_CONSOLE_PATH} is used in {@code web.xml}</li>
030 * </ul>
031 * <p/>
032 * All constants are public and fields protected for easy extension of the class. Following protected methods
033 * and classes are provided to override the default function:
034 * <ul>
035 * <li>{@link #shouldBeReported} - compares actual request nano time with {@link #getThreshold(javax.servlet.http.HttpServletRequest)}
036 * (which may become unused if this method is overridden)</li>
037 * <li>{@link #getThreshold(javax.servlet.http.HttpServletRequest)} - returns threshold configured in {@code web.xml}</li>
038 * <li>{@link org.javasimon.javaee.reqreporter.RequestReporter} can be implemented and specified using init parameter {@link #INIT_PARAM_REQUEST_REPORTER_CLASS}</li>
039 * <li>{@link HttpStopwatchSource} can be subclassed and specified using init parameter {@link #INIT_PARAM_STOPWATCH_SOURCE_CLASS}, specifically
040 * following methods are intended for override:
041 * <ul>
042 * <li>{@link HttpStopwatchSource#isMonitored(javax.servlet.http.HttpServletRequest)} - true except for request with typical resource suffixes
043 * ({@code .gif}, {@code .jpg}, {@code .css}, etc.)</li>
044 * <li>{@link HttpStopwatchSource#getMonitorName(javax.servlet.http.HttpServletRequest)}</li>
045 * </ul></li>
046 * </ul>
047 *
048 * @author <a href="mailto:virgo47@gmail.com">Richard "Virgo" Richter</a>
049 * @since 2.3
050 */
051 @SuppressWarnings("UnusedParameters")
052 public class SimonServletFilter implements Filter {
053 /**
054 * Name of filter init parameter for Simon name prefix.
055 */
056 public static final String INIT_PARAM_PREFIX = "prefix";
057
058 /**
059 * Name of filter init parameter that sets the value of threshold in milliseconds for maximal
060 * request duration beyond which all splits will be dumped to log. The actual threshold can be
061 * further customized overriding {@link #getThreshold(javax.servlet.http.HttpServletRequest)} method,
062 * but this parameter has to be set to non-null value to enable threshold reporting feature (0 for instance).
063 */
064 public static final String INIT_PARAM_REPORT_THRESHOLD_MS = "report-threshold-ms";
065
066 /**
067 * Name of filter init parameter that sets relative ULR path that will provide Simon console page.
068 * If the parameter is not used, basic plain text console will be disabled.
069 */
070 public static final String INIT_PARAM_SIMON_CONSOLE_PATH = "console-path";
071
072 /**
073 * FQN of the Stopwatch source class implementing {@link org.javasimon.source.MonitorSource}.
074 * One can use {@link DisabledMonitorSource} to disabled monitoring.
075 * Defaults to {@link HttpStopwatchSource}.
076 */
077 public static final String INIT_PARAM_STOPWATCH_SOURCE_CLASS = "stopwatch-source-class";
078
079 /**
080 * Enable/disable caching on Stopwatch resolution.
081 * <em>Warning: as the cache key is the {@link HttpServletRequest#getRequestURI()},
082 * this is incompatible with application passing data in their
083 * request URI, this is often the case of RESTful services.
084 * For instance "/car/1023/driver" and "/car/3624/driver"
085 * may point to the same page but with different URLs.</em>
086 * Defaults to {@code false}.
087 */
088 public static final String INIT_PARAM_STOPWATCH_SOURCE_CACHE = "stopwatch-source-cache";
089
090 /**
091 * FQN of the {@link org.javasimon.javaee.reqreporter.RequestReporter} implementation that is used to report requests
092 * that {@link #shouldBeReported(javax.servlet.http.HttpServletRequest, long, java.util.List)}.
093 * Default is {@link org.javasimon.javaee.reqreporter.DefaultRequestReporter}.
094 */
095 public static final String INIT_PARAM_REQUEST_REPORTER_CLASS = "request-reporter-class";
096
097 /**
098 * Properties for a StopwatchSource class. Has the following format: prop1=val1;prop2=val2
099 * Properties are assumed to be correct Java bean properties and should exist in a class specified by
100 * {@link org.javasimon.javaee.SimonServletFilter#INIT_PARAM_STOPWATCH_SOURCE_CLASS}
101 */
102 public static final String INIT_PARAM_STOPWATCH_SOURCE_PROPS = "stopwatch-source-props";
103
104 private static Replacer FINAL_SLASH_REMOVE = new Replacer("/*$", "");
105
106 private static Replacer SLASH_TRIM = new Replacer("^/*(.*?)/*$", "$1");
107
108 /**
109 * Threshold in ns - any request longer than this will be reported by current {@link #requestReporter} instance.
110 * Specified by {@link #INIT_PARAM_REPORT_THRESHOLD_MS} ({@value #INIT_PARAM_REPORT_THRESHOLD_MS}) in the {@code web.xml} (in ms,
111 * converted to ns during servlet init). This is the default value returned by {@link #getThreshold(javax.servlet.http.HttpServletRequest)}
112 * but it may be completely ignored if method is overridden so. However if the field is {@code null} threshold reporting feature
113 * is disabled.
114 */
115 protected Long reportThresholdNanos;
116
117 /**
118 * URL path that displays Simon tree - it is console-path without the ending slash.
119 */
120 protected String printTreePath;
121
122 /**
123 * URL path that displays Simon web console (or null if no console is required).
124 */
125 protected String consolePath;
126
127 /**
128 * Simon Manager used by the filter.
129 */
130 private Manager manager = SimonManager.manager();
131
132 /**
133 * Thread local list of splits used to cumulate all splits for the request.
134 * Every instance of the Servlet has its own thread-local to bind its lifecycle to
135 * the callback that servlet is registering. Then even more callbacks registered from various
136 * servlets in the same manager do not interfere.
137 */
138 private final ThreadLocal<List<Split>> splitsThreadLocal = new ThreadLocal<List<Split>>();
139
140 /**
141 * Callback that saves all splits in {@link #splitsThreadLocal} if {@link #reportThresholdNanos} is configured.
142 */
143 private SplitSaverCallback splitSaverCallback;
144
145 /**
146 * Stopwatch source is used before/after each request to start/stop a stopwatch.
147 */
148 private StopwatchSource<HttpServletRequest> stopwatchSource;
149
150 /**
151 * Object responsible for reporting the request over threshold (if {@link #shouldBeReported(javax.servlet.http.HttpServletRequest, long, java.util.List)}
152 * returns true).
153 */
154 private RequestReporter requestReporter;
155
156 /**
157 * Initialization method that processes various init parameters from {@literal web.xml} and sets manager, if
158 * {@link org.javasimon.utils.SimonUtils#MANAGER_SERVLET_CTX_ATTRIBUTE} servlet context attribute is not {@code null}.
159 *
160 * @param filterConfig filter config object
161 */
162 public final void init(FilterConfig filterConfig) {
163 pickUpSharedManagerIfExists(filterConfig);
164 stopwatchSource = SimonServletFilterUtils.initStopwatchSource(filterConfig, manager);
165 setStopwatchSourceProperties(filterConfig, stopwatchSource);
166
167 requestReporter = SimonServletFilterUtils.initRequestReporter(filterConfig);
168 requestReporter.setSimonServletFilter(this);
169
170 String reportThreshold = filterConfig.getInitParameter(INIT_PARAM_REPORT_THRESHOLD_MS);
171 if (reportThreshold != null) {
172 try {
173 this.reportThresholdNanos = Long.parseLong(reportThreshold) * ClockUtils.NANOS_IN_MILLIS;
174 splitSaverCallback = new SplitSaverCallback();
175 manager.callback().addCallback(splitSaverCallback);
176 } catch (NumberFormatException e) {
177 // ignore
178 }
179 }
180
181 String consolePath = filterConfig.getInitParameter(INIT_PARAM_SIMON_CONSOLE_PATH);
182 if (consolePath != null) {
183 this.printTreePath = FINAL_SLASH_REMOVE.process(consolePath);
184 this.consolePath = printTreePath + "/";
185 }
186 }
187
188 private void setStopwatchSourceProperties(FilterConfig filterConfig, StopwatchSource<HttpServletRequest> stopwatchSource) {
189 registerEnumConverter();
190
191 String properties = filterConfig.getInitParameter(INIT_PARAM_STOPWATCH_SOURCE_PROPS);
192 for (String keyValStr : properties.split(";")) {
193 String[] keyVal = keyValStr.split("=");
194 String key = keyVal[0];
195 String val = keyVal[1];
196
197 SimonBeanUtils.getInstance().setProperty(stopwatchSource, key, val);
198 }
199 }
200
201 private void registerEnumConverter() {
202 SimonBeanUtils.getInstance().registerConverter(HttpStopwatchSource.IncludeHttpMethodName.class, new ToEnumConverter());
203 }
204
205 private void pickUpSharedManagerIfExists(FilterConfig filterConfig) {
206 Object managerObject = filterConfig.getServletContext().getAttribute(SimonUtils.MANAGER_SERVLET_CTX_ATTRIBUTE);
207 if (managerObject != null && managerObject instanceof Manager) {
208 manager = (Manager) managerObject;
209 }
210 }
211
212 /**
213 * Wraps the HTTP request with Simon measuring. Separate Simons are created for different URIs (parameters
214 * ignored).
215 *
216 * @param servletRequest HTTP servlet request
217 * @param servletResponse HTTP servlet response
218 * @param filterChain filter chain
219 * @throws IOException possibly thrown by other filter/servlet in the chain
220 * @throws ServletException possibly thrown by other filter/servlet in the chain
221 */
222 public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
223 HttpServletRequest request = (HttpServletRequest) servletRequest;
224 HttpServletResponse response = (HttpServletResponse) servletResponse;
225
226 String localPath = request.getRequestURI().substring(request.getContextPath().length());
227 if (consolePath != null && (localPath.equals(printTreePath) || localPath.startsWith(consolePath))) {
228 consolePage(request, response, localPath);
229 return;
230 }
231
232 doFilterWithMonitoring(filterChain, request, response);
233 }
234
235 private void doFilterWithMonitoring(FilterChain filterChain, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
236 Split split = stopwatchSource.start(request);
237 if (split.isEnabled() && reportThresholdNanos != null) {
238 splitsThreadLocal.set(new ArrayList<Split>());
239 }
240
241 try {
242 filterChain.doFilter(request, response);
243 // TODO: is it sensible to catch exceptions here and stop split with tags?
244 // for instance Wicket does not let the exception go to here anyway
245 } finally {
246 stopSplitForRequest(request, split);
247 }
248 }
249
250 private void stopSplitForRequest(HttpServletRequest request, Split split) {
251 if (split.isEnabled()) {
252 split.stop();
253 long splitNanoTime = split.runningFor();
254 if (reportThresholdNanos != null) {
255 List<Split> splits = splitsThreadLocal.get();
256 splitsThreadLocal.remove(); // better do this before we call potentially overridden method
257 if (shouldBeReported(request, splitNanoTime, splits)) {
258 requestReporter.reportRequest(request, split, splits);
259 }
260 }
261 }
262 }
263
264 /**
265 * Determines whether the request is over the threshold - with all incoming parameters this method can be
266 * very flexible. Default implementation just compares the actual requestNanoTime with
267 * {@link #getThreshold(javax.servlet.http.HttpServletRequest)} (which by default returns value configured
268 * in {@code web.xml})
269 *
270 * @param request HTTP servlet request
271 * @param requestNanoTime actual HTTP request nano time
272 * @param splits all splits started for the request
273 * @return {@code true}, if request should be reported as over threshold
274 */
275 protected boolean shouldBeReported(HttpServletRequest request, long requestNanoTime, List<Split> splits) {
276 return requestNanoTime > getThreshold(request);
277 }
278
279 /**
280 * Returns actual threshold in *nanoseconds* (not ms as configured) which allows to further customize threshold per request - intended for override.
281 * Default behavior returns configured {@link #reportThresholdNanos} (already converted to ns).
282 *
283 * @param request HTTP Request
284 * @return threshold in ns for current request
285 * @since 3.2
286 */
287 protected long getThreshold(HttpServletRequest request) {
288 return reportThresholdNanos;
289 }
290
291 private void consolePage(HttpServletRequest request, HttpServletResponse response, String localPath) throws IOException {
292 response.setContentType("text/plain");
293 response.setHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
294 response.setHeader("Pragma", "no-cache");
295
296 if (localPath.equals(printTreePath)) {
297 printSimonTree(response);
298 return;
299 }
300
301 String subCommand = SLASH_TRIM.process(localPath.substring(consolePath.length()));
302 if (subCommand.isEmpty()) {
303 printSimonTree(response);
304 } else if (subCommand.equalsIgnoreCase("clearManager")) {
305 manager.clear();
306 response.getOutputStream().println("Simon Manager was cleared");
307 } else if (subCommand.equalsIgnoreCase("help")) {
308 simonHelp(response);
309 } else {
310 response.getOutputStream().println("Invalid command\n");
311 simonHelp(response);
312 }
313 }
314
315 private void simonHelp(ServletResponse response) throws IOException {
316 response.getOutputStream().println("Simon Console help - available commands:");
317 response.getOutputStream().println("- clearManager - clears the manager (removes all Simons)");
318 response.getOutputStream().println("- help - shows this help");
319 }
320
321 private void printSimonTree(ServletResponse response) throws IOException {
322 response.getOutputStream().println(SimonUtils.simonTreeString(manager.getRootSimon()));
323 }
324
325 public Manager getManager() {
326 return manager;
327 }
328
329 /**
330 * Returns stopwatch source used by the filter.
331 *
332 * @return stopwatch source
333 */
334 StopwatchSource<HttpServletRequest> getStopwatchSource() {
335 return stopwatchSource;
336 }
337
338 /**
339 * Removes the splitSaverCallback if initialized.
340 */
341 public void destroy() {
342 if (splitSaverCallback != null) {
343 manager.callback().removeCallback(splitSaverCallback);
344 }
345 }
346
347 private class SplitSaverCallback extends CallbackSkeleton {
348 @Override
349 public void onStopwatchStart(Split split) {
350 List<Split> splits = splitsThreadLocal.get();
351 if (splits != null) {
352 splits.add(split);
353 }
354 }
355 }
356 }