View Javadoc

1   /* Copyright (c) 2008-2009 HomeAway, Inc.
2    * All rights reserved.  http://www.perf4j.org
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.perf4j.log4j;
17  
18  import org.apache.log4j.Appender;
19  import org.apache.log4j.AppenderSkeleton;
20  import org.apache.log4j.Level;
21  import org.apache.log4j.Logger;
22  import org.apache.log4j.helpers.AppenderAttachableImpl;
23  import org.apache.log4j.spi.AppenderAttachable;
24  import org.apache.log4j.spi.LoggingEvent;
25  import org.perf4j.GroupedTimingStatistics;
26  import org.perf4j.StopWatch;
27  import org.perf4j.helpers.GenericAsyncCoalescingStatisticsAppender;
28  
29  import java.util.Enumeration;
30  import java.io.Flushable;
31  
32  /**
33   * This log4j Appender groups StopWatch log messages together to form GroupedTimingStatistics. At a scheduled interval
34   * the StopWatch log messages that currently exist in the buffer are pulled to create a single
35   * GroupedTimingStatistics instance that is then sent to any attached appenders.
36   * <p/>
37   * Note that any LoggingEvents which do NOT contain StopWatch objects are discarded. Also, this appender stores logged
38   * messages in a bounded buffer before sending those messages to downstream appenders. If the buffer becomes full then
39   * subsequent logs will be discarded until the buffer has time to clear. You can access the number of discarded
40   * messages using the getNumDiscardedMessages() method.
41   *
42   * @author Alex Devine
43   */
44  public class AsyncCoalescingStatisticsAppender extends AppenderSkeleton implements AppenderAttachable {
45      // --- configuration options ---
46      // note most configuration options are provided by the GenericAsyncCoalescingStatisticsAppender
47      /**
48       * DownstreamLogLevel option, converted to a Level object
49       */
50      private Level downstreamLogLevel = Level.INFO;
51  
52      // --- contained objects ---
53      /**
54       * This instance provides the main logic for this appender. This wrapper class just provides the log4j-specific
55       * parts.
56       */
57      private final GenericAsyncCoalescingStatisticsAppender baseImplementation =
58              newGenericAsyncCoalescingStatisticsAppender();
59  
60      /**
61       * The downstream appenders are contained in this AppenderAttachableImpl
62       */
63      private final AppenderAttachableImpl downstreamAppenders = new AppenderAttachableImpl();
64  
65      /**
66       * This shutdown hook is needed to flush the appender on JVM shutdown so that all messages are logged.
67       */
68      private Thread shutdownHook = null;
69  
70      // --- options ---
71      /**
72       * The <b>TimeSlice</b> option represents the length of time, in milliseconds, of the window in which appended
73       * LoggingEvents are coalesced to a single GroupedTimingStatistics and sent to downstream appenders.
74       * Defaults to 30,000 milliseconds.
75       *
76       * @return the TimeSlice option.
77       */
78      public long getTimeSlice() {
79          return baseImplementation.getTimeSlice();
80      }
81  
82      /**
83       * Sets the value of the <b>TimeSlice</b> option.
84       *
85       * @param timeSlice The new TimeSlice option, in milliseconds.
86       */
87      public void setTimeSlice(long timeSlice) {
88          baseImplementation.setTimeSlice(timeSlice);
89      }
90  
91      /**
92       * The <b>DownstreamLogLevel</b> option gets the Level of the GroupedTimingStatistics LoggingEvent that is sent to
93       * downstream appenders. Since each GroupedTimingStatistics represents a view of a collection of single StopWatch
94       * timing event, each of which may have been logged at different levels, this appender needs to decide on a single
95       * Level to use to notify downstream appenders. Defaults to "INFO".
96       *
97       * @return The DownstreamLogLevel option as a String
98       */
99      public String getDownstreamLogLevel() {
100         return downstreamLogLevel.toString();
101     }
102 
103     /**
104      * Sets the value of the <b>DownstreamLogLevel</b> option. This String must be one of the defined Level constants.
105      *
106      * @param downstreamLogLevel The new DownstreamLogLevel option.
107      */
108     public void setDownstreamLogLevel(String downstreamLogLevel) {
109         this.downstreamLogLevel = Level.toLevel(downstreamLogLevel);
110     }
111 
112     /**
113      * The <b>CreateRollupStatistics</b> option is used to determine whether "rollup" statistics should be created.
114      * If the tag name of a StopWatch in a log message contains periods, then the GroupedTimingStatistics will be
115      * created as if each substring of the tag up to the period was also logged with a separate StopWatch instance.
116      * For example, suppose a StopWatch was logged with a tag of "requests.specificReq.PASS". For grouping purposes
117      * a StopWatch entry would be logged under each of the following tags:
118      * <ul>
119      * <li>requests
120      * <li>requests.specificReq
121      * <li>requests.specificReq.PASS
122      * </ul>
123      * This allows you to view statistics at both an individual and aggregated level. If there were other StopWatch
124      * entries with a tag of "requests.specificReq.FAIL", then the data collected at the "requests.specificReq" level
125      * would include BOTH PASS and FAIL events.
126      *
127      * @return The CreateRollupStatistics option.
128      */
129     public boolean isCreateRollupStatistics() {
130         return baseImplementation.isCreateRollupStatistics();
131     }
132 
133     /**
134      * Sets the value of the <b>CreateRollupStatistics</b> option.
135      *
136      * @param createRollupStatistics The new CreateRollupStatistics option.
137      */
138     public void setCreateRollupStatistics(boolean createRollupStatistics) {
139         baseImplementation.setCreateRollupStatistics(createRollupStatistics);
140     }
141 
142     /**
143      * The <b>QueueSize</b> option is used to control the size of the internal queue used by this appender to store
144      * logged messages before they are sent to downstream appenders. Defaults to 1024. If set too small and the queue
145      * fills up, then logged StopWatches will be discarded. The number of discarded messages can be accessed using the
146      * {@link #getNumDiscardedMessages()} method.
147      *
148      * @return The QueueSize option.
149      */
150     public int getQueueSize() {
151         return baseImplementation.getQueueSize();
152     }
153 
154     /**
155      * Sets the value of the <b>QueueSize</b> option.
156      *
157      * @param queueSize The new QueueSize option.
158      */
159     public void setQueueSize(int queueSize) {
160         baseImplementation.setQueueSize(queueSize);
161     }
162 
163     /**
164      * The <b>StopWatchParserClassName</b> option is used to determine the class used to parse stop watch messages
165      * into StopWatch instances. This defaults to the standard "org.perf4j.helpers.StopWatchParser" class.
166      *
167      * @return The StopWatchParserClassName option.
168      */
169     public String getStopWatchParserClassName() {
170         return baseImplementation.getStopWatchParserClassName();
171     }
172 
173     /**
174      * Sets the value of the <b>StopWatchParserClassName</b> option.
175      *
176      * @param stopWatchParserClassName The new StopWatchParserClassName option.
177      */
178     public void setStopWatchParserClassName(String stopWatchParserClassName) {
179         baseImplementation.setStopWatchParserClassName(stopWatchParserClassName);
180     }
181 
182     public void setName(String name) {
183         super.setName(name);
184         baseImplementation.setName(name);
185     }
186 
187     public synchronized void activateOptions() {
188         //Start the underlying generic appender with a handler object that pumps statistics to the downstream appenders
189         baseImplementation.start(new GenericAsyncCoalescingStatisticsAppender.GroupedTimingStatisticsHandler() {
190             public void handle(GroupedTimingStatistics statistics) {
191                 LoggingEvent coalescedLoggingEvent =
192                         new LoggingEvent(Logger.class.getName(),
193                                          Logger.getLogger(StopWatch.DEFAULT_LOGGER_NAME),
194                                          System.currentTimeMillis(),
195                                          downstreamLogLevel,
196                                          statistics,
197                                          null);
198                 try {
199                     synchronized (downstreamAppenders) {
200                         downstreamAppenders.appendLoopOnAppenders(coalescedLoggingEvent);
201                     }
202                 } catch (Exception e) {
203                     getErrorHandler().error(
204                             "Exception calling append with GroupedTimingStatistics on downstream appender",
205                             e, -1, coalescedLoggingEvent
206                     );
207                 }
208             }
209 
210             public void error(String errorMessage) {
211                 getErrorHandler().error(errorMessage);
212             }
213         });
214 
215         //Also, we add a shutdown hook that will attempt to flush any pending log events in the queue.
216         if (shutdownHook == null) {
217             try {
218                 Runtime.getRuntime().addShutdownHook(shutdownHook = new Thread("perf4j-async-stats-appender-shutdown") {
219                     public void run() {
220                         if (!closed) {
221                             close();
222                         }
223                     }
224                 });
225             } catch (Exception e) { /* likely a security exception, nothing we can do */ }
226         }
227     }
228 
229     // --- attributes ---
230     /**
231      * Returns the number of StopWatch messages that have been discarded due to the queue being full.
232      *
233      * @return The number of discarded messages.
234      */
235     public int getNumDiscardedMessages() {
236         return baseImplementation.getNumDiscardedMessages();
237     }
238 
239     // --- appender attachable methods ---
240 
241     public void addAppender(Appender appender) {
242         synchronized (downstreamAppenders) {
243             downstreamAppenders.addAppender(appender);
244         }
245     }
246 
247     public Enumeration getAllAppenders() {
248         synchronized (downstreamAppenders) {
249             return downstreamAppenders.getAllAppenders();
250         }
251     }
252 
253     public Appender getAppender(String name) {
254         synchronized (downstreamAppenders) {
255             return downstreamAppenders.getAppender(name);
256         }
257     }
258 
259     public boolean isAttached(Appender appender) {
260         synchronized (downstreamAppenders) {
261             return downstreamAppenders.isAttached(appender);
262         }
263     }
264 
265     public void removeAllAppenders() {
266         synchronized (downstreamAppenders) {
267             downstreamAppenders.removeAllAppenders();
268         }
269     }
270 
271     public void removeAppender(Appender appender) {
272         synchronized (downstreamAppenders) {
273             downstreamAppenders.removeAppender(appender);
274         }
275     }
276 
277     public void removeAppender(String name) {
278         synchronized (downstreamAppenders) {
279             downstreamAppenders.removeAppender(name);
280         }
281     }
282 
283     // --- appender methods ---
284     protected void append(LoggingEvent event) {
285         baseImplementation.append(String.valueOf(event.getMessage()));
286     }
287 
288     public boolean requiresLayout() {
289         return false;
290     }
291 
292     public void close() {
293         baseImplementation.stop();
294 
295         //close the downstream appenders
296         synchronized (downstreamAppenders) {
297             //first FLUSH any flushable downstream appenders (fix for PERFFORJ-22). Note we CAN NOT just flush and
298             //close in one loop because this breaks in the case of a "diamond" relationship between appenders, where,
299             //say, this appender has 2 attached GraphingStatisticsAppenders that each write to a SINGLE attached
300             //FileAppender.
301             for (Enumeration enumer = downstreamAppenders.getAllAppenders();
302                  enumer != null && enumer.hasMoreElements();) {
303                 Appender appender = (Appender) enumer.nextElement();
304                 if (appender instanceof Flushable) {
305                     try {
306                         ((Flushable)appender).flush();
307                     } catch (Exception e) { /* Just eat the exception, we're closing down */ }
308                 }
309             }
310 
311             //THEN close them
312             for (Enumeration enumer = downstreamAppenders.getAllAppenders();
313                  enumer != null && enumer.hasMoreElements();) {
314                 ((Appender) enumer.nextElement()).close();
315             }
316         }
317 
318         this.closed = true;
319     }
320 
321     // --- helper methods ---
322     /**
323      * Creates the new GenericAsyncCoalescingStatisticsAppender that this instance will wrap.
324      *
325      * @return The newly created GenericAsyncCoalescingStatisticsAppender.
326      */
327     protected GenericAsyncCoalescingStatisticsAppender newGenericAsyncCoalescingStatisticsAppender() {
328         return new GenericAsyncCoalescingStatisticsAppender();
329     }
330 }