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    }