View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.log4j.chainsaw;
18  
19  import org.apache.log4j.AppenderSkeleton;
20  import org.apache.log4j.LogManager;
21  import org.apache.log4j.helpers.Constants;
22  import org.apache.log4j.rule.ExpressionRule;
23  import org.apache.log4j.rule.Rule;
24  import org.apache.log4j.spi.LoggerRepositoryEx;
25  import org.apache.log4j.spi.LoggingEvent;
26  import org.apache.log4j.spi.LoggingEventFieldResolver;
27  
28  import javax.swing.event.EventListenerList;
29  import java.beans.PropertyChangeListener;
30  import java.beans.PropertyChangeSupport;
31  import java.util.*;
32  
33  /**
34   * A handler class that either extends a particular appender hierarchy or can be
35   * bound into the Log4j appender framework, and queues events, to be later
36   * dispatched to registered/interested parties.
37   *
38   * @author Scott Deboy <sdeboy@apache.org>
39   * @author Paul Smith <psmith@apache.org>
40   */
41  public class ChainsawAppenderHandler extends AppenderSkeleton {
42      private static final String DEFAULT_IDENTIFIER = "Unknown";
43      private final Object mutex = new Object();
44      private int sleepInterval = 1000;
45      private EventListenerList listenerList = new EventListenerList();
46      private double dataRate = 0.0;
47      private String identifierExpression;
48      private final LoggingEventFieldResolver resolver = LoggingEventFieldResolver
49          .getInstance();
50      private PropertyChangeSupport propertySupport = new PropertyChangeSupport(
51          this);
52      private Map<String, Rule> customExpressionRules = new HashMap<>();
53  
54      /**
55       * NOTE: This variable needs to be physically located LAST, because
56       * of the initialization sequence, the WorkQueue constructor starts a thread
57       * which ends up needing some reference to fields created in ChainsawAppenderHandler (outer instance)
58       * which may not have been created yet.  Becomes a race condition, and therefore
59       * this field initialization should be kept last.
60       */
61      private WorkQueue worker = new WorkQueue();
62  
63      public ChainsawAppenderHandler(final ChainsawAppender appender) {
64          super(true);
65          appender.setAppender(this);
66      }
67  
68      public ChainsawAppenderHandler() {
69          super(true);
70      }
71  
72      public void setIdentifierExpression(String identifierExpression) {
73          synchronized (mutex) {
74              this.identifierExpression = identifierExpression;
75              mutex.notify();
76          }
77      }
78  
79      public String getIdentifierExpression() {
80          return identifierExpression;
81      }
82  
83      public void addCustomEventBatchListener(String identifier,
84                                              EventBatchListener l) throws IllegalArgumentException {
85          customExpressionRules.put(identifier, ExpressionRule.getRule(identifier));
86          listenerList.add(EventBatchListener.class, l);
87      }
88  
89      public void addEventBatchListener(EventBatchListener l) {
90          listenerList.add(EventBatchListener.class, l);
91      }
92  
93      public void removeEventBatchListener(EventBatchListener l) {
94          listenerList.remove(EventBatchListener.class, l);
95      }
96  
97      public void append(LoggingEvent event) {
98          worker.enqueue(event);
99      }
100 
101     public void close() {
102     }
103 
104     public boolean requiresLayout() {
105         return false;
106     }
107 
108     public int getQueueInterval() {
109         return sleepInterval;
110     }
111 
112     public void setQueueInterval(int interval) {
113         sleepInterval = interval;
114     }
115 
116     /**
117      * Determines an appropriate title for the Tab for the Tab Pane by locating a
118      * the hostname property
119      *
120      * @param e
121      * @return identifier
122      */
123     String getTabIdentifier(LoggingEvent e) {
124         String ident = resolver.applyFields(identifierExpression, e);
125         return ((ident != null) ? ident : DEFAULT_IDENTIFIER);
126     }
127 
128     /**
129      * Exposes the current Data rate calculated. This is periodically updated by
130      * an internal Thread as is the number of events that have been processed, and
131      * dispatched to all listeners since the last sample period divided by the
132      * number of seconds since the last sample period.
133      * <p>
134      * This method fires a PropertyChange event so listeners can monitor the rate
135      *
136      * @return double # of events processed per second
137      */
138     public double getDataRate() {
139         return dataRate;
140     }
141 
142     /**
143      * @param dataRate
144      */
145     void setDataRate(double dataRate) {
146         double oldValue = this.dataRate;
147         this.dataRate = dataRate;
148         propertySupport.firePropertyChange("dataRate", oldValue,
149             this.dataRate);
150     }
151 
152     /**
153      * @param listener
154      */
155     public synchronized void addPropertyChangeListener(
156         PropertyChangeListener listener) {
157         propertySupport.addPropertyChangeListener(listener);
158     }
159 
160     /**
161      * @param propertyName
162      * @param listener
163      */
164     public synchronized void addPropertyChangeListener(String propertyName,
165                                                        PropertyChangeListener listener) {
166         propertySupport.addPropertyChangeListener(propertyName, listener);
167     }
168 
169     /**
170      * @param listener
171      */
172     public synchronized void removePropertyChangeListener(
173         PropertyChangeListener listener) {
174         propertySupport.removePropertyChangeListener(listener);
175     }
176 
177     /**
178      * @param propertyName
179      * @param listener
180      */
181     public synchronized void removePropertyChangeListener(String propertyName,
182                                                           PropertyChangeListener listener) {
183         propertySupport.removePropertyChangeListener(propertyName, listener);
184     }
185 
186     /**
187      * Queue of Events are placed in here, which are picked up by an asychronous
188      * thread. The WorkerThread looks for events once a second and processes all
189      * events accumulated during that time..
190      */
191     class WorkQueue {
192         final ArrayList<LoggingEvent> queue = new ArrayList<>();
193         Thread workerThread;
194 
195         protected WorkQueue() {
196             workerThread = new WorkerThread();
197             workerThread.start();
198         }
199 
200         public final void enqueue(LoggingEvent event) {
201             synchronized (mutex) {
202                 queue.add(event);
203                 mutex.notify();
204             }
205         }
206 
207         public final void stop() {
208             synchronized (mutex) {
209                 workerThread.interrupt();
210             }
211         }
212 
213         /**
214          * The worker thread converts each queued event to a vector and forwards the
215          * vector on to the UI.
216          */
217         private class WorkerThread extends Thread {
218             public WorkerThread() {
219                 super("Chainsaw-WorkerThread");
220                 setDaemon(true);
221                 setPriority(Thread.NORM_PRIORITY - 1);
222             }
223 
224             public void run() {
225                 List<LoggingEvent> innerList = new ArrayList<>();
226                 while (true) {
227                     long timeStart = System.currentTimeMillis();
228                     synchronized (mutex) {
229                         try {
230                             while ((queue.size() == 0) || (identifierExpression == null)) {
231                                 setDataRate(0);
232                                 mutex.wait();
233                             }
234                             if (queue.size() > 0) {
235                                 innerList.addAll(queue);
236                                 queue.clear();
237                             }
238                         } catch (InterruptedException ie) {
239                         }
240                     }
241                     int size = innerList.size();
242                     if (size > 0) {
243                         Iterator<LoggingEvent> iter = innerList.iterator();
244                         ChainsawEventBatch eventBatch = new ChainsawEventBatch();
245                         while (iter.hasNext()) {
246                             LoggingEvent e = iter.next();
247                             // attempt to set the host name (without port), from
248                             // remoteSourceInfo
249                             // if 'hostname' property not provided
250                             if (e.getProperty(Constants.HOSTNAME_KEY) == null) {
251                                 String remoteHost = e
252                                     .getProperty(ChainsawConstants.LOG4J_REMOTEHOST_KEY);
253                                 if (remoteHost != null) {
254                                     int colonIndex = remoteHost.indexOf(":");
255                                     if (colonIndex == -1) {
256                                         colonIndex = remoteHost.length();
257                                     }
258                                     e.setProperty(Constants.HOSTNAME_KEY, remoteHost.substring(0,
259                                         colonIndex));
260                                 }
261                             }
262                             for (Object o : customExpressionRules.entrySet()) {
263                                 Map.Entry entry = (Map.Entry) o;
264                                 Rule rule = (Rule) entry.getValue();
265                                 if (rule.evaluate(e, null)) {
266                                     eventBatch.addEvent((String) entry.getKey(), e);
267                                 }
268                             }
269                             eventBatch.addEvent(getTabIdentifier(e), e);
270                         }
271                         dispatchEventBatch(eventBatch);
272                         innerList.clear();
273                     }
274                     if (getQueueInterval() > 1000) {
275                         try {
276                             synchronized (this) {
277                                 wait(getQueueInterval());
278                             }
279                         } catch (InterruptedException ie) {
280                         }
281                     } else {
282                         Thread.yield();
283                     }
284                     if (size == 0) {
285                         setDataRate(0.0);
286                     } else {
287                         long timeEnd = System.currentTimeMillis();
288                         long diffInSeconds = (timeEnd - timeStart) / 1000;
289                         double rate = (((double) size) / diffInSeconds);
290                         setDataRate(rate);
291                     }
292                 }
293             }
294 
295             /**
296              * Dispatches the event batches contents to all the interested parties by
297              * iterating over each identifier and dispatching the
298              * ChainsawEventBatchEntry object to each listener that is interested.
299              *
300              * @param eventBatch
301              */
302             private void dispatchEventBatch(ChainsawEventBatch eventBatch) {
303                 EventBatchListener[] listeners = listenerList
304                     .getListeners(EventBatchListener.class);
305                 for (Iterator<String> iter = eventBatch.identifierIterator(); iter.hasNext(); ) {
306                     String identifier = iter.next();
307                     List<LoggingEvent> eventList = null;
308                     for (EventBatchListener listener : listeners) {
309                         if ((listener.getInterestedIdentifier() == null)
310                             || listener.getInterestedIdentifier().equals(identifier)) {
311                             if (eventList == null) {
312                                 eventList = eventBatch.entrySet(identifier);
313                             }
314                             listener.receiveEventBatch(identifier, eventList);
315                         }
316                     }
317                 }
318             }
319         }
320     }
321 }