001 package hirondelle.web4j.webmaster;
002
003 import hirondelle.web4j.util.Stopwatch;
004 import hirondelle.web4j.util.Util;
005 import hirondelle.web4j.util.WebUtil;
006
007 import java.io.IOException;
008 import java.util.Enumeration;
009 import java.util.LinkedList;
010 import java.util.List;
011 import java.util.logging.Logger;
012
013 import javax.servlet.Filter;
014 import javax.servlet.FilterChain;
015 import javax.servlet.FilterConfig;
016 import javax.servlet.ServletException;
017 import javax.servlet.ServletRequest;
018 import javax.servlet.ServletResponse;
019 import javax.servlet.http.HttpServletRequest;
020
021 /**
022 Compile simple performance statistics, and use periodic pings to detect trouble.
023
024 <P>See <tt>web.xml</tt> for more information on how to configure this {@link Filter}.
025
026 <h3>Performance Statistics</h3>
027 This class stores a <tt>Collection</tt> of {@link PerformanceSnapshot} objects in
028 memory (not in a database).
029
030 <P>The presentation of these performance statistics in a JSP is always "one behind" this class.
031 This {@link Filter} examines the response time of each <em>fully processed</em>
032 request. Any JSP presenting the response times, however, is not fully processed <i>from the
033 point of view of this filter</i>, and has not yet contributed to the statistics.
034
035 <P><span class="highlight">It is important to note that {@link Filter} objects
036 must be designed to operate safely in a multi-threaded environment</span>.
037 Using the <a href='http://www.javapractices.com/Topic48.cjp'>nomenclature</a> of
038 <em>Effective Java</em>, this class is 'conditionally thread safe' : the responsibility
039 for correct operation in a multi-threaded environment is <em>shared</em> between
040 this class and its caller. See {@link #getPerformanceHistory} for more information.
041
042 <P> If desired, you can use also external tools such as
043 <a href='http://www.siteuptime.com/'>SiteUptime.com</a> to monitor your site.
044 */
045 public final class PerformanceMonitor implements Filter {
046
047 /**
048 Read in the configuration of this filter from <tt>web.xml</tt>.
049
050 <P>The config is validated, gathering of statistics is begun, and
051 any periodic ping operations are initialized.
052 */
053 public void init(FilterConfig aFilterConfig) {
054 /*
055 The logging performed here is not showing up in the expected manner.
056 */
057 Enumeration items = aFilterConfig.getInitParameterNames();
058 while ( items.hasMoreElements() ) {
059 String name = (String)items.nextElement();
060 String value = aFilterConfig.getInitParameter(name);
061 fLogger.fine("Filter param " + name + " = " + Util.quote(value));
062 }
063
064 fEXPOSURE_TIME = new Integer( aFilterConfig.getInitParameter(EXPOSURE_TIME) );
065 fNUM_PERFORMANCE_SNAPSHOTS = new Integer(
066 aFilterConfig.getInitParameter(NUM_PERFORMANCE_SNAPSHOTS)
067 );
068 validateConfigParamValues();
069
070 fPerformanceHistory.addFirst(new PerformanceSnapshot(fEXPOSURE_TIME));
071 }
072
073 /** This implementation does nothing. */
074 public void destroy() {
075 //do nothing
076 }
077
078 /** Calculate server response time, and store relevant statistics in memory. */
079 public void doFilter(ServletRequest aRequest, ServletResponse aResponse, FilterChain aChain) throws IOException, ServletException {
080 fLogger.fine("START PerformanceMonitor Filter.");
081
082 Stopwatch stopwatch = new Stopwatch();
083 stopwatch.start();
084
085 aChain.doFilter(aRequest, aResponse);
086
087 stopwatch.stop();
088 addResponseTime(stopwatch.toValue(), aRequest);
089 fLogger.fine("END PerformanceMonitor Filter. Response Time: " + stopwatch);
090 }
091
092 /**
093 Return statistics on recent application performance.
094
095 <P>A static method is the only way an {@link hirondelle.web4j.action.Action}
096 can access this data, since it has no access to the {@link Filter} object
097 itself (which is built by the container).
098
099 <P>The typical task for the caller is iteration over the return value. The caller
100 <b>must</b> synchronize this iteration, by obtaining the lock on the return value.
101 The typical use case of this method is :
102 <PRE>
103 List history = PerformanceMonitor.getPerformanceHistory();
104 synchronized(history) {
105 for(PerformanceSnapshot snapshot : history){
106 //..elided
107 }
108 }
109 </PRE>
110 */
111 public static List<PerformanceSnapshot> getPerformanceHistory(){
112 /*
113 Note that using Collections.synchronizedList here is not possible : the
114 API for that method states that when used, the returned reference must be used
115 for ALL interactions with the backing list.
116 */
117 return fPerformanceHistory;
118 }
119
120 // PRIVATE
121
122 /**
123 Holds queue of {@link PerformanceSnapshot} objects, including the "current" one.
124
125 <P>The queue grows until it reaches a configured maximum length, after which stale
126 items are removed when new ones are added.
127
128 <P>This mutable item must always have synchronized access, to ensure thread-safety.
129 */
130 private static final LinkedList<PerformanceSnapshot> fPerformanceHistory = new LinkedList<PerformanceSnapshot>();
131
132 private static Integer fEXPOSURE_TIME;
133 private static Integer fNUM_PERFORMANCE_SNAPSHOTS;
134
135 /*
136 Names of configuration parameters.
137 */
138 private static final String NUM_PERFORMANCE_SNAPSHOTS = "NumPerformanceSnapshots";
139 private static final String EXPOSURE_TIME = "ExposureTime";
140
141 private static final Logger fLogger = Util.getLogger(PerformanceMonitor.class);
142
143 /**
144 Validate the configured parameter values.
145 */
146 private void validateConfigParamValues(){
147 StringBuilder message = new StringBuilder();
148 if ( ! Util.isInRange(fNUM_PERFORMANCE_SNAPSHOTS, 1, 1000) ) {
149 message.append(
150 "web.xml: " + NUM_PERFORMANCE_SNAPSHOTS + " has value of " +
151 fNUM_PERFORMANCE_SNAPSHOTS + ", which is outside the accepted range of 1..1000."
152 );
153 }
154 int exposure = fEXPOSURE_TIME;
155 if ( exposure != 10 && exposure != 20 && exposure != 30 && exposure != 60){
156 message.append(
157 " web.xml: " + EXPOSURE_TIME + " has a value of " + exposure + "." +
158 " The only accepted values are 10, 20, 30, and 60."
159 );
160 }
161 if ( Util.textHasContent(message.toString()) ){
162 throw new IllegalArgumentException(message.toString());
163 }
164 }
165
166 private static void addResponseTime(long aResponseTime, ServletRequest aRequest){
167 long now = System.currentTimeMillis();
168 HttpServletRequest request = (HttpServletRequest)aRequest;
169 String url = WebUtil.getURLWithQueryString(request);
170 //this single synchronization block implements the *internal* thread-safety
171 //responsibilities of this class
172 synchronized( fPerformanceHistory ){
173 if ( now > getCurrentSnapshot().getEndTime().getTime() ){
174 //start a new 'current' snapshot
175 addToPerformanceHistory( new PerformanceSnapshot(fEXPOSURE_TIME) );
176 }
177 updateCurrentSnapshotStats(aResponseTime, url);
178 }
179 }
180
181 private static void addToPerformanceHistory(PerformanceSnapshot aNewSnapshot){
182 while ( hasGap(aNewSnapshot, getCurrentSnapshot() ) ) {
183 fLogger.fine("Gap detected. Adding empty snapshot.");
184 PerformanceSnapshot filler = PerformanceSnapshot.forGapInActivity(getCurrentSnapshot());
185 addNewSnapshot(filler);
186 }
187 addNewSnapshot(aNewSnapshot);
188 }
189
190 private static boolean hasGap(PerformanceSnapshot aNewSnapshot, PerformanceSnapshot aCurrentSnapshot){
191 return aNewSnapshot.getEndTime().getTime() - aCurrentSnapshot.getEndTime().getTime() > fEXPOSURE_TIME*60*1000;
192 }
193
194 private static void addNewSnapshot(PerformanceSnapshot aNewSnapshot){
195 fPerformanceHistory.addFirst(aNewSnapshot);
196 ensureSizeRemainsLimited();
197 }
198
199 private static void ensureSizeRemainsLimited() {
200 if ( fPerformanceHistory.size() > fNUM_PERFORMANCE_SNAPSHOTS ){
201 fPerformanceHistory.removeLast();
202 }
203 }
204
205 private static PerformanceSnapshot getCurrentSnapshot(){
206 return fPerformanceHistory.getFirst();
207 }
208
209 private static void updateCurrentSnapshotStats(long aResponseTime, String aURL){
210 PerformanceSnapshot updatedSnapshot = getCurrentSnapshot().addResponseTime(
211 aResponseTime, aURL
212 );
213 //this style is needed only because the PerfomanceSnapshot objects are immutable.
214 //Immutability is advantageous, since it guarantees that the caller
215 //cannot change the internal state of this class.
216 fPerformanceHistory.removeFirst();
217 fPerformanceHistory.addFirst(updatedSnapshot);
218 }
219 }