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    }