001package org.javasimon.javaee; 002 003import org.javasimon.Manager; 004import org.javasimon.SimonManager; 005import org.javasimon.Split; 006import org.javasimon.callback.CallbackSkeleton; 007import org.javasimon.clock.SimonClock; 008import org.javasimon.javaee.reqreporter.RequestReporter; 009import org.javasimon.source.DisabledMonitorSource; 010import org.javasimon.source.StopwatchSource; 011import org.javasimon.utils.Replacer; 012import org.javasimon.utils.SimonUtils; 013import org.javasimon.utils.bean.SimonBeanUtils; 014import org.javasimon.utils.bean.ToEnumConverter; 015 016import javax.servlet.*; 017import javax.servlet.http.HttpServletRequest; 018import javax.servlet.http.HttpServletResponse; 019import java.io.IOException; 020import java.util.ArrayList; 021import 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") 052public 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<>(); 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 {@code 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) * SimonClock.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 String properties = filterConfig.getInitParameter(INIT_PARAM_STOPWATCH_SOURCE_PROPS); 190 if (properties == null) { 191 return; 192 } 193 194 registerEnumConverter(); 195 for (String keyValStr : properties.split(";")) { 196 String[] keyVal = keyValStr.split("="); 197 String key = keyVal[0]; 198 String val = keyVal[1]; 199 200 SimonBeanUtils.getInstance().setProperty(stopwatchSource, key, val); 201 } 202 } 203 204 private void registerEnumConverter() { 205 SimonBeanUtils.getInstance().registerConverter(HttpStopwatchSource.IncludeHttpMethodName.class, new ToEnumConverter()); 206 } 207 208 private void pickUpSharedManagerIfExists(FilterConfig filterConfig) { 209 Object managerObject = filterConfig.getServletContext().getAttribute(SimonUtils.MANAGER_SERVLET_CTX_ATTRIBUTE); 210 if (managerObject != null && managerObject instanceof Manager) { 211 manager = (Manager) managerObject; 212 } 213 } 214 215 /** 216 * Wraps the HTTP request with Simon measuring. Separate Simons are created for different URIs (parameters 217 * ignored). 218 * 219 * @param servletRequest HTTP servlet request 220 * @param servletResponse HTTP servlet response 221 * @param filterChain filter chain 222 * @throws IOException possibly thrown by other filter/servlet in the chain 223 * @throws ServletException possibly thrown by other filter/servlet in the chain 224 */ 225 public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 226 HttpServletRequest request = (HttpServletRequest) servletRequest; 227 HttpServletResponse response = (HttpServletResponse) servletResponse; 228 229 String localPath = request.getRequestURI().substring(request.getContextPath().length()); 230 if (consolePath != null && (localPath.equals(printTreePath) || localPath.startsWith(consolePath))) { 231 consolePage(request, response, localPath); 232 return; 233 } 234 235 doFilterWithMonitoring(filterChain, request, response); 236 } 237 238 private void doFilterWithMonitoring(FilterChain filterChain, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { 239 Split split = stopwatchSource.start(request); 240 if (split.isEnabled() && reportThresholdNanos != null) { 241 splitsThreadLocal.set(new ArrayList<Split>()); 242 } 243 244 try { 245 filterChain.doFilter(request, response); 246 // TODO: is it sensible to catch exceptions here and stop split with tags? 247 // for instance Wicket does not let the exception go to here anyway 248 } finally { 249 stopSplitForRequest(request, split); 250 } 251 } 252 253 private void stopSplitForRequest(HttpServletRequest request, Split split) { 254 if (split.isEnabled()) { 255 split.stop(); 256 long splitNanoTime = split.runningFor(); 257 if (reportThresholdNanos != null) { 258 List<Split> splits = splitsThreadLocal.get(); 259 splitsThreadLocal.remove(); // better do this before we call potentially overridden method 260 if (shouldBeReported(request, splitNanoTime, splits)) { 261 requestReporter.reportRequest(request, split, splits); 262 } 263 } 264 } 265 } 266 267 /** 268 * Determines whether the request is over the threshold - with all incoming parameters this method can be 269 * very flexible. Default implementation just compares the actual requestNanoTime with 270 * {@link #getThreshold(javax.servlet.http.HttpServletRequest)} (which by default returns value configured 271 * in {@code web.xml}) 272 * 273 * @param request HTTP servlet request 274 * @param requestNanoTime actual HTTP request nano time 275 * @param splits all splits started for the request 276 * @return {@code true}, if request should be reported as over threshold 277 */ 278 protected boolean shouldBeReported(HttpServletRequest request, long requestNanoTime, List<Split> splits) { 279 return requestNanoTime > getThreshold(request); 280 } 281 282 /** 283 * Returns actual threshold in *nanoseconds* (not ms as configured) which allows to further customize threshold per request - intended for override. 284 * Default behavior returns configured {@link #reportThresholdNanos} (already converted to ns). 285 * 286 * @param request HTTP Request 287 * @return threshold in ns for current request 288 * @since 3.2 289 */ 290 protected long getThreshold(HttpServletRequest request) { 291 return reportThresholdNanos; 292 } 293 294 private void consolePage(HttpServletRequest request, HttpServletResponse response, String localPath) throws IOException { 295 response.setContentType("text/plain"); 296 response.setHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); 297 response.setHeader("Pragma", "no-cache"); 298 299 if (localPath.equals(printTreePath)) { 300 printSimonTree(response); 301 return; 302 } 303 304 String subCommand = SLASH_TRIM.process(localPath.substring(consolePath.length())); 305 if (subCommand.isEmpty()) { 306 printSimonTree(response); 307 } else if (subCommand.equalsIgnoreCase("clearManager")) { 308 manager.clear(); 309 response.getOutputStream().println("Simon Manager was cleared"); 310 } else if (subCommand.equalsIgnoreCase("help")) { 311 simonHelp(response); 312 } else { 313 response.getOutputStream().println("Invalid command\n"); 314 simonHelp(response); 315 } 316 } 317 318 private void simonHelp(ServletResponse response) throws IOException { 319 response.getOutputStream().println("Simon Console help - available commands:"); 320 response.getOutputStream().println("- clearManager - clears the manager (removes all Simons)"); 321 response.getOutputStream().println("- help - shows this help"); 322 } 323 324 private void printSimonTree(ServletResponse response) throws IOException { 325 response.getOutputStream().println(SimonUtils.simonTreeString(manager.getRootSimon())); 326 } 327 328 public Manager getManager() { 329 return manager; 330 } 331 332 /** 333 * Returns stopwatch source used by the filter. 334 * 335 * @return stopwatch source 336 */ 337 StopwatchSource<HttpServletRequest> getStopwatchSource() { 338 return stopwatchSource; 339 } 340 341 /** 342 * Removes the splitSaverCallback if initialized. 343 */ 344 public void destroy() { 345 if (splitSaverCallback != null) { 346 manager.callback().removeCallback(splitSaverCallback); 347 } 348 } 349 350 private class SplitSaverCallback extends CallbackSkeleton { 351 @Override 352 public void onStopwatchStart(Split split) { 353 List<Split> splits = splitsThreadLocal.get(); 354 if (splits != null) { 355 splits.add(split); 356 } 357 } 358 } 359}