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 }