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 }