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        long millis = stopwatch.toValue()/(1000*1000);
089        addResponseTime(millis, aRequest);
090        fLogger.fine("END PerformanceMonitor Filter. Response Time: " + stopwatch);
091      }
092    
093      /**
094       Return statistics on recent application performance.
095      
096       <P>A static method is the only way an {@link hirondelle.web4j.action.Action} 
097       can access this data, since it has no access to the {@link Filter} object 
098       itself (which is built by the container).
099       
100       <P>The typical task for the caller is iteration over the return value. The caller  
101       <b>must</b> synchronize this iteration, by obtaining the lock on the return value. 
102       The typical use case of this method is :
103       <PRE>
104       List history = PerformanceMonitor.getPerformanceHistory();
105       synchronized(history) {
106         for(PerformanceSnapshot snapshot : history){
107           //..elided
108         }
109       }
110       </PRE>
111      */
112      public static List<PerformanceSnapshot> getPerformanceHistory(){
113        /*
114         Note that using Collections.synchronizedList here is not possible : the 
115         API for that method states that when used, the returned reference must be used 
116         for ALL interactions with the backing list. 
117        */
118        return fPerformanceHistory;
119      }
120      
121      // PRIVATE 
122      
123      /**
124       Holds queue of {@link PerformanceSnapshot} objects, including the "current" one.
125      
126      <P>The queue grows until it reaches a configured maximum length, after which stale 
127       items are removed when new ones are added.
128      
129       <P>This mutable item must always have synchronized access, to ensure thread-safety.
130      */
131      private static final LinkedList<PerformanceSnapshot> fPerformanceHistory = new LinkedList<PerformanceSnapshot>();
132      
133      private static Integer fEXPOSURE_TIME;
134      private static Integer fNUM_PERFORMANCE_SNAPSHOTS;
135      
136      /*
137       Names of configuration parameters.
138      */
139      private static final String NUM_PERFORMANCE_SNAPSHOTS = "NumPerformanceSnapshots";
140      private static final String EXPOSURE_TIME = "ExposureTime";
141      
142      private static final Logger fLogger = Util.getLogger(PerformanceMonitor.class);
143      
144      /**
145       Validate the configured parameter values.
146      */
147      private void validateConfigParamValues(){
148        StringBuilder message = new StringBuilder();
149        if ( ! Util.isInRange(fNUM_PERFORMANCE_SNAPSHOTS, 1, 1000) ) {
150          message.append(
151            "web.xml: " + NUM_PERFORMANCE_SNAPSHOTS + " has value of " + 
152            fNUM_PERFORMANCE_SNAPSHOTS + ", which is outside the accepted range of 1..1000."
153          );
154        }
155        int exposure = fEXPOSURE_TIME;
156        if ( exposure != 10 && exposure != 20 && exposure != 30 && exposure != 60){
157          message.append(
158            " web.xml: " + EXPOSURE_TIME + " has a value of " + exposure + "." + 
159            " The only accepted values are 10, 20, 30, and 60."
160          );
161        }
162        if ( Util.textHasContent(message.toString()) ){
163          throw new IllegalArgumentException(message.toString());
164        }
165      }
166      
167      private static void addResponseTime(long aResponseTime /*millis*/, ServletRequest aRequest){
168        long now = System.currentTimeMillis();
169        HttpServletRequest request = (HttpServletRequest)aRequest;
170        String url = WebUtil.getURLWithQueryString(request);
171        //this single synchronization block implements the *internal* thread-safety 
172        //responsibilities of this class
173        synchronized( fPerformanceHistory ){
174          if ( now > getCurrentSnapshot().getEndTime().getTime() ){
175            //start a new 'current' snapshot
176            addToPerformanceHistory( new PerformanceSnapshot(fEXPOSURE_TIME) );
177          }
178          updateCurrentSnapshotStats(aResponseTime, url);
179        }
180      }
181      
182      private static void addToPerformanceHistory(PerformanceSnapshot aNewSnapshot){
183        while ( hasGap(aNewSnapshot, getCurrentSnapshot() ) ) {
184          fLogger.fine("Gap detected. Adding empty snapshot.");
185          PerformanceSnapshot filler = PerformanceSnapshot.forGapInActivity(getCurrentSnapshot());
186          addNewSnapshot(filler);
187        }
188        addNewSnapshot(aNewSnapshot);
189      }
190    
191      private static boolean hasGap(PerformanceSnapshot aNewSnapshot, PerformanceSnapshot aCurrentSnapshot){
192        return aNewSnapshot.getEndTime().getTime() - aCurrentSnapshot.getEndTime().getTime() > fEXPOSURE_TIME*60*1000;
193      }
194      
195      private static void addNewSnapshot(PerformanceSnapshot aNewSnapshot){
196        fPerformanceHistory.addFirst(aNewSnapshot);
197        ensureSizeRemainsLimited();
198      }
199      
200      private static void ensureSizeRemainsLimited() {
201        if ( fPerformanceHistory.size() > fNUM_PERFORMANCE_SNAPSHOTS ){
202          fPerformanceHistory.removeLast();
203        }
204      }
205      
206      private static PerformanceSnapshot getCurrentSnapshot(){
207        return fPerformanceHistory.getFirst();
208      }
209      
210      private static void updateCurrentSnapshotStats(long aResponseTime /*millis*/, String aURL){
211        PerformanceSnapshot updatedSnapshot = getCurrentSnapshot().addResponseTime(
212          aResponseTime, aURL
213        );
214        //this style is needed only because the PerfomanceSnapshot objects are immutable.
215        //Immutability is advantageous, since it guarantees that the caller 
216        //cannot change the internal state of this class.
217        fPerformanceHistory.removeFirst();
218        fPerformanceHistory.addFirst(updatedSnapshot);
219      }
220    }