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}