Package hirondelle.web4j.webmaster

Source Code of hirondelle.web4j.webmaster.PerformanceMonitor

package hirondelle.web4j.webmaster;

import hirondelle.web4j.util.Stopwatch;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.util.WebUtil;
import java.io.IOException;
import java.util.*;
import java.util.logging.Logger;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

/**
Compile simple performance statistics, and use periodic pings to detect trouble.

<P>See <tt>web.xml</tt> for more information on how to configure this {@link Filter}.
<h3>Performance Statistics</h3>
This class stores a <tt>Collection</tt> of {@link PerformanceSnapshot} objects in
memory (not in a database).

<P>The presentation of these performance statistics in a JSP is always "one behind" this class.
This {@link Filter} examines the response time of each <em>fully processed</em>
request. Any JSP presenting the response times, however, is not fully processed <i>from the
point of view of this filter</i>, and has not yet contributed to the statistics. 

<P><span class="highlight">It is important to note that {@link Filter} objects
must be designed to operate safely in a multi-threaded environment</span>
Using the <a href='http://www.javapractices.com/Topic48.cjp'>nomenclature</a> of
<em>Effective Java</em>, this class is 'conditionally thread safe' : the responsibility
for correct operation in a multi-threaded environment is <em>shared</em> between
this class and its caller. See {@link #getPerformanceHistory} for more information.
<h3>Periodic Pings</h3>
You can optionally have this filter do periodic pings on an application
URL, by adding an <tt>init-param</tt> to the filter config of the form:
<PRE>
{@code
<init-param> 
  <param-name>BadResponseDetector</param-name>
  <param-value>http://www.blah.com/whatever.do, 5, 30</param-value>
</init-param>
}
</PRE>
The <tt>BadRequestDetector</tt> setting specifies 3 items separated by a comma:
<ol>
<li>a single target URL. This target URL should not require user login,
and should represent an action which accesses the database.
<li>a ping frequency (in minutes, range <tt>1..60</tt>).
<li>a timeout value (in seconds, range <tt>1..60</tt>).
</ol>

<P>Pings begin shortly after this filter is first initialized.
Each ping does a <tt>GET</tt> for the target URL in a
background thread. These ping operations look for two kinds of problems :
<ul>
<li>HTTP responses that return an error code of 400 or more (which 
indicate an error of some sort).
<li>response times that exceed the configured timeout.
</ul>

<P>If a problem is detected, a short {@link hirondelle.web4j.webmaster.TroubleTicket}
  describing the problem is sent to the <tt>TroubleTicketMailingList</tt>
  recipients configured in <tt>web.xml</tt>.
  
<P>Applications may not respond normally to HTTP requests for various reasons :
<ul>
<li>DNS problems.
<li>database connection problems.
<li>memory or resource problems on the server.
<li>problems in the communication between Apache and Tomcat (for example).
<li>and so on...
</ul>
<P>This class runs as part of your application. Of course, it's unable to detect when
your app itself is not running. However, it's still quite useful, and will
catch many important errors. If desired, you can use sites such as
<a href='http://www.siteuptime.com/'>SiteUptime.com</a> to monitor your site
using external tools.
*/
public final class PerformanceMonitor implements Filter {
 
  /**
   Read in the configuration of this filter from <tt>web.xml</tt>.
  
   <P>The config is validated, gathering of statistics is begun, and
   any periodic ping operations are initialized.
  */
  public void init(FilterConfig aFilterConfig) {
    /*
     The logging performed here is not showing up in the expected manner.
    */
    Enumeration items = aFilterConfig.getInitParameterNames();
    while ( items.hasMoreElements() ) {
      String name = (String)items.nextElement();
      String value = aFilterConfig.getInitParameter(name);
      fLogger.fine("Filter param " + name + " = " + Util.quote(value));
    }
   
    fEXPOSURE_TIME = new Integer( aFilterConfig.getInitParameter(EXPOSURE_TIME) );
    fNUM_PERFORMANCE_SNAPSHOTS = new Integer(
      aFilterConfig.getInitParameter(NUM_PERFORMANCE_SNAPSHOTS)
    );
    validateConfigParamValues();
   
    fPerformanceHistory.addFirst(new PerformanceSnapshot(fEXPOSURE_TIME));
   
    String badRequestConfig = aFilterConfig.getInitParameter(BAD_RESPONSE_DETECTOR);
    if( Util.textHasContent(badRequestConfig)){
      fLogger.fine("Starting BadRequestDetector.");
      startBadResponseDetector(badRequestConfig);
    }
    else {
      fLogger.fine("Not starting BadRequestDetector.");
    }
  }
 
  /** This implementation does nothing.  */
  public void destroy() {
    //do nothing
  }
 
  /** Calculate server response time, and store relevant statistics in memory.   */
  public void doFilter(ServletRequest aRequest, ServletResponse aResponse, FilterChain aChain) throws IOException, ServletException {
    fLogger.fine("START PerformanceMonitor Filter.");
   
    if( ! fHasLoggedBadReqDetectorDetails ) {
      logBadResponseDetectorDetails();
    }
   
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();
   
    aChain.doFilter(aRequest, aResponse);
   
    stopwatch.stop();
    addResponseTime(stopwatch.toValue(), aRequest);
    //fLogger.fine(fPerformanceHistory.toString());
    fLogger.fine("END PerformanceMonitor Filter. Response Time: " + stopwatch);
  }

  /**
   Return statistics on recent application performance.
 
   <P>A static method is the only way an {@link hirondelle.web4j.action.Action}
   can access this data, since it has no access to the {@link Filter} object
   itself (which is built by the container).
  
   <P>The typical task for the caller is iteration over the return value. The caller 
   <b>must</b> synchronize this iteration, by obtaining the lock on the return value.
   The typical use case of this method is :
   <PRE>
   List history = PerformanceMonitor.getPerformanceHistory();
   synchronized(history) {
     for(PerformanceSnapshot snapshot : history){
       //..elided
     }
   }
   </PRE>
  */
  public static List<PerformanceSnapshot> getPerformanceHistory(){
    /*
     Note that using Collections.synchronizedList here is not possible : the
     API for that method states that when used, the returned reference must be used
     for ALL interactions with the backing list.
    */
    return fPerformanceHistory;
  }
 
  // PRIVATE //
 
  private BadResponseDetector fBadResponseDetector;
  private boolean fHasLoggedBadReqDetectorDetails;
 
  /**
   Holds queue of {@link PerformanceSnapshot} objects, including the "current" one.
 
  <P>The queue grows until it reaches a configured maximum length, after which stale
   items are removed when new ones are added.
 
   <P>This mutable item must always have synchronized access, to ensure thread-safety.
  */
  private static final LinkedList<PerformanceSnapshot> fPerformanceHistory = new LinkedList<PerformanceSnapshot>();
 
  private static Integer fEXPOSURE_TIME;
  private static Integer fNUM_PERFORMANCE_SNAPSHOTS;
 
  /*
   Names of configuration parameters.
  */
  private static final String NUM_PERFORMANCE_SNAPSHOTS = "NumPerformanceSnapshots";
  private static final String EXPOSURE_TIME = "ExposureTime";
  private static final String BAD_RESPONSE_DETECTOR = "BadResponseDetector";
 
  private static final Logger fLogger = Util.getLogger(PerformanceMonitor.class);
 
  /**
   Validate the configured parameter values.
  */
  private void validateConfigParamValues(){
    StringBuilder message = new StringBuilder();
    if ( ! Util.isInRange(fNUM_PERFORMANCE_SNAPSHOTS, 1, 1000) ) {
      message.append(
        "web.xml: " + NUM_PERFORMANCE_SNAPSHOTS + " has value of " +
        fNUM_PERFORMANCE_SNAPSHOTS + ", which is outside the accepted range of 1..1000."
      );
    }
    int exposure = fEXPOSURE_TIME;
    if ( exposure != 10 && exposure != 20 && exposure != 30 && exposure != 60){
      message.append(
        " web.xml: " + EXPOSURE_TIME + " has a value of " + exposure + "." +
        " The only accepted values are 10, 20, 30, and 60."
      );
    }
    if ( Util.textHasContent(message.toString()) ){
      throw new IllegalArgumentException(message.toString());
    }
  }
 
  private static void addResponseTime(long aResponseTime, ServletRequest aRequest){
    long now = System.currentTimeMillis();
    HttpServletRequest request = (HttpServletRequest)aRequest;
    String url = WebUtil.getURLWithQueryString(request);
    //String url = request.getRequestURL().toString();
    //this single synchronization block implements the *internal* thread-safety
    //responsibilities of this class
    synchronized( fPerformanceHistory ){
      if ( now > getCurrentSnapshot().getEndTime().getTime() ){
        //start a new 'current' snapshot
        addToPerformanceHistory( new PerformanceSnapshot(fEXPOSURE_TIME) );
      }
      updateCurrentSnapshotStats(aResponseTime, url);
    }
  }
 
  private static void addToPerformanceHistory(PerformanceSnapshot aNewSnapshot){
    while ( hasGap(aNewSnapshot, getCurrentSnapshot() ) ) {
      fLogger.fine("Gap detected. Adding empty snapshot.");
      PerformanceSnapshot filler = PerformanceSnapshot.forGapInActivity(getCurrentSnapshot());
      addNewSnapshot(filler);
    }
    addNewSnapshot(aNewSnapshot);
  }

  private static boolean hasGap(PerformanceSnapshot aNewSnapshot, PerformanceSnapshot aCurrentSnapshot){
    return aNewSnapshot.getEndTime().getTime() - aCurrentSnapshot.getEndTime().getTime() > fEXPOSURE_TIME*60*1000;
  }
 
  private static void addNewSnapshot(PerformanceSnapshot aNewSnapshot){
    fPerformanceHistory.addFirst(aNewSnapshot);
    ensureSizeRemainsLimited();
  }
 
  private static void ensureSizeRemainsLimited() {
    if ( fPerformanceHistory.size() > fNUM_PERFORMANCE_SNAPSHOTS ){
      fPerformanceHistory.removeLast();
    }
  }
 
  private static PerformanceSnapshot getCurrentSnapshot(){
    return fPerformanceHistory.getFirst();
  }
 
  private static void updateCurrentSnapshotStats(long aResponseTime, String aURL){
    PerformanceSnapshot updatedSnapshot = getCurrentSnapshot().addResponseTime(
      aResponseTime, aURL
    );
    //this style is needed only because the PerfomanceSnapshot objects are immutable.
    //Immutability is advantageous, since it guarantees that the caller
    //cannot change the internal state of this class.
    fPerformanceHistory.removeFirst();
    fPerformanceHistory.addFirst(updatedSnapshot);
  }

  private void startBadResponseDetector(String aConfig) {
    fLogger.fine("Starting BadResponseDetector using Timer...");
    fBadResponseDetector = BadResponseDetector.getInstanceUsing(aConfig);
    Timer timer = new Timer();
    Date startTime = startOneMinuteFromNow();
    timer.scheduleAtFixedRate(fBadResponseDetector, startTime, fBadResponseDetector.getPingFrequency() * 60 * 1000);
  }
 
  private Date startOneMinuteFromNow(){
    Calendar desiredInstant = new GregorianCalendar();
    desiredInstant.add(Calendar.MINUTE, 1);
    Calendar result = new GregorianCalendar(
      desiredInstant.get(Calendar.YEAR),
      desiredInstant.get(Calendar.MONTH),
      desiredInstant.get(Calendar.DAY_OF_MONTH),
      desiredInstant.get(Calendar.HOUR_OF_DAY),
      desiredInstant.get(Calendar.MINUTE),
      desiredInstant.get(Calendar.SECOND)
    );
    return result.getTime();
  }
 
  private void logBadResponseDetectorDetails() {
    if( fBadResponseDetector != null ) {
      fLogger.fine(
        "BadResponseDetector has been activated for " + fBadResponseDetector.getTargetURL() +
        ". It executes every " + fBadResponseDetector.getPingFrequency() + " minute(s)," +
        " and uses a timeout of " + fBadResponseDetector.getTimeout() + " second(s)."
      );
    }
    else {
      fLogger.fine("BadResponseDetector has not been activated.");
    }
    fHasLoggedBadReqDetectorDetails = true;
  }
}
TOP

Related Classes of hirondelle.web4j.webmaster.PerformanceMonitor

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.