Shahzad Bhatti

March 17, 2010

Smarter Email appender for Log4j with support of duplicate-removal, summary-report and JMX

Filed under: Computing — admin @ 5:06 pm

I have been using SMTPAppender for a while to notify developers when something breaks on the production site and for most part it works well. However, due to some misconfiguration or service crash it can result in large number of emails. I was struck by similar problem at work when my email box suddently got tons of emails from the production site. So I decided to write a bit intelligent email appender. My goals for the appender were:

  • Throttle emails based on some configured time
  • Remove duplicate emails
  • Support JMX for dynamic configuration
  • Provide summary report with count of errors and their timings

I created FilteredSMTPAppender class that extends SMTPAppender. The FilteredSMTPAppender defines a nested class Stats for keeping track of errors. For each unique exception, it creates an instance of Stats, that stores the first and last occurrence of this exception as well as count. The Stats class uses hash of stack trace to identify unique exceptions, however it ignores first line, which often stores some dynamic information. FilteredSMTPAppender registers iteslf as MBean so that it can be configured at runtime. It overrides append method to capture the event and overrides checkEntryConditions to add filtering. It also changes the layout so that the summary count of error messages are added to the footer of email message.

The FilteredSMTPAppender uses a number of helper classes such as ServiceJMXBeanImpl for MBean definition, LRUSortedList to keep fixed cache of exceptions. Here is listing of LRUSortedList and ServiceJMXBeanImpl.

Listing of FilteredSMTPAppender.java

   1 package com.plexobject.log;
 
   2 
   3 import java.beans.PropertyChangeEvent;
   4 import java.beans.PropertyChangeListener;
   5 import java.util.Comparator;
 
   6 import java.util.Date;
   7 
   8 import javax.mail.MessagingException;
   9 
 
  10 import org.apache.commons.lang.builder.EqualsBuilder;
  11 import org.apache.commons.lang.time.FastDateFormat;
  12 
  13 import org.apache.log4j.Layout;
 
  14 import org.apache.log4j.net.SMTPAppender;
  15 import org.apache.log4j.spi.LoggingEvent;
  16 
  17 import com.plexobject.jmx.JMXRegistrar;
 
  18 import com.plexobject.jmx.impl.ServiceJMXBeanImpl;
  19 import com.plexobject.metrics.Metric;
  20 import com.plexobject.metrics.Timer;
 
  21 import com.plexobject.util.Configuration;
  22 import com.plexobject.util.LRUSortedList;
  23 
  24 public class FilteredSMTPAppender extends SMTPAppender {
 
  25 
  26     private static final String SMTP_FILTER_MIN_DUPLICATE_INTERVAL_SECS = "smtp.filter.min.duplicate.interval.secs";
  27     private static final int MAX_STATS = Configuration.getInstance().getInteger("smtp.filter.max", 100);
 
  28     private static int MIN_DUPLICATE_EMAILS_INTERVAL = Configuration.getInstance().getInteger(SMTP_FILTER_MIN_DUPLICATE_INTERVAL_SECS,
  29             60); // 1 minute
  30     private static final Date STARTED = new Date();
 
  31     private static final FastDateFormat DATE_FMT = FastDateFormat.getInstance("MM/dd/yy HH:mm");
  32 
  33     final static class Stats implements Comparable<Stats> {
 
  34 
  35         final int checksum;
  36         final long firstSeen;
 
  37         long lastSeen;
  38         long lastSent;
  39         int numSeen;
 
  40         int numEmails;
  41 
  42         Stats(LoggingEvent event) {
  43             StringBuilder sb = new StringBuilder();
 
  44             String[] trace = event.getThrowableStrRep();
  45             for (int i = 1; i < trace.length && i < 20; i++) { // top 20 lines
 
  46                 // of trace
  47                 sb.append(trace[i].trim());
  48             }
  49             this.checksum = sb.toString().hashCode();
 
  50             firstSeen = lastSeen = System.currentTimeMillis();
  51             numSeen = 1;
  52         }
  53 
  54         boolean check() {
 
  55             long current = System.currentTimeMillis();
  56             long elapsed = current - lastSent;
  57 
  58             numSeen++;
 
  59             lastSeen = current;
  60 
  61             if (elapsed > MIN_DUPLICATE_EMAILS_INTERVAL * 1000) {
  62                 lastSent = current;
 
  63                 numEmails++;
  64                 return true;
  65             } else {
 
  66                 return false;
  67             }
  68         }
  69 
 
  70         @Override
  71         public boolean equals(Object object) {
  72             if (!(object instanceof Stats)) {
 
  73                 return false;
  74             }
  75             Stats rhs = (Stats) object;
  76             return new EqualsBuilder().append(this.checksum, rhs.checksum).isEquals();
 
  77 
  78         }
  79 
  80         @Override
  81         public int hashCode() {
 
  82             return checksum;
  83         }
  84 
  85         @Override
 
  86         public String toString() {
  87             return " (" + checksum + ") occurred " + numSeen + " times, " + numEmails + " # of emails, first @" + DATE_FMT.format(new Date(firstSeen)) + ", last @" + DATE_FMT.format(new Date(lastSeen)) + " since server started @" + DATE_FMT.format(STARTED);
 
  88         }
  89 
  90         @Override
  91         public int compareTo(Stats other) {
 
  92             return checksum - other.checksum;
  93         }
  94     }
  95 
 
  96     final static class StatsCmp implements Comparator<Stats> {
  97 
 
  98         @Override
  99         public int compare(Stats first, Stats second) {
 100             return first.checksum - second.checksum;
 
 101         }
 102     }
 103     private static final LRUSortedList<Stats> STATS_LIST = new LRUSortedList<Stats>(
 
 104             MAX_STATS, new StatsCmp());
 105     private LoggingEvent event;
 106     private ServiceJMXBeanImpl mbean;
 107     private Layout layout;
 
 108 
 109     public FilteredSMTPAppender() {
 110         mbean = JMXRegistrar.getInstance().register(getClass());
 111         mbean.addPropertyChangeListener(new PropertyChangeListener() {
 112 
 
 113             @Override
 114             public void propertyChange(PropertyChangeEvent event) {
 115                 try {
 116                     if (event != null && SMTP_FILTER_MIN_DUPLICATE_INTERVAL_SECS.equalsIgnoreCase(event.getPropertyName())) {
 
 117                         MIN_DUPLICATE_EMAILS_INTERVAL = Integer.parseInt((String) event.getNewValue());
 118                     }
 119                 } catch (Exception e) {
 120                     e.printStackTrace();
 121                 }
 
 122             }
 123         });
 124 
 125     }
 126 
 127     public void append(LoggingEvent event) {
 
 128         this.event = event;
 129         if (layout == null) {
 130             layout = getLayout();
 131         }
 
 132         super.append(event);
 133     }
 134 
 135     protected boolean checkEntryConditions() {
 136         final Timer timer = Metric.newTimer(getClass().getSimpleName() + ".checkEntryConditions");
 
 137         try {
 138             boolean check = true;
 139             if (event != null) {
 
 140                 Stats newStats = new Stats(event);
 141                 Stats stats = STATS_LIST.get(newStats);
 142                 if (stats == null) {
 143                     stats = newStats;
 
 144                     STATS_LIST.add(stats);
 145                 } else {
 146                     check = stats.check();
 147                 }
 148                 if (check) {
 
 149                     setMessageFooter(stats);
 150                 }
 151             }
 152             return check && super.checkEntryConditions();
 
 153         } finally {
 154             timer.stop();
 155         }
 156     }
 157 
 
 158     private void setMessageFooter(Stats stats) {
 159         String message = event.getMessage().toString();
 160 
 161         final String footer = "\n\n-------------------------\n" + message + " - " + stats;
 
 162 
 163         if (layout != null) {
 164             setLayout(new Layout() {
 165 
 
 166                 @Override
 167                 public void activateOptions() {
 168                     layout.activateOptions();
 169 
 170                 }
 
 171 
 172                 @Override
 173                 public String format(LoggingEvent evt) {
 174                     return layout.format(evt);
 175                 }
 
 176 
 177                 @Override
 178                 public String getFooter() {
 179                     return footer;
 180                 }
 
 181 
 182                 @Override
 183                 public boolean ignoresThrowable() {
 184                     return layout.ignoresThrowable();
 
 185                 }
 186             });
 187         }
 188     }
 189 }
 190 
 
 191 
 

Listing of ServiceJMXBeanImpl.java

   1 package com.plexobject.util;
 
   2 
   3 import java.util.ArrayList;
   4 import java.util.Collection;
   5 import java.util.Collections;
 
   6 import java.util.Comparator;
   7 import java.util.Iterator;
   8 import java.util.List;
 
   9 import java.util.ListIterator;
  10 
  11 import org.apache.log4j.Logger;
  12 
 
  13 
  14 public class LRUSortedList<T> implements List<T> {
 
  15     private static final Logger LOGGER = Logger.getLogger(LRUSortedList.class);
  16     private final int max;
 
  17     private final Comparator<T> comparator;
  18 
  19     private final List<Pair<Long, T>> list = new ArrayList<Pair<Long, T>>();
 
  20     private final List<Pair<Long, Integer>> timestamps = new ArrayList<Pair<Long, Integer>>();
 
  21 
  22     // comparator to sort by timestamp
  23     private static final Comparator<Pair<Long, Integer>> CMP = new Comparator<Pair<Long, Integer>>() {
 
  24         @Override
  25         public int compare(Pair<Long, Integer> first, Pair<Long, Integer> second) {
 
  26             if (first.getFirst() < second.getFirst()) {
  27                 return -1;
  28             } else if (first.getFirst() > second.getFirst()) {
 
  29                 return 1;
  30             } else {
  31                 return 0;
 
  32             }
  33         }
  34     };
  35 
  36     public LRUSortedList(int max, Comparator<T> comparator) {
 
  37         this.max = max;
  38         this.comparator = comparator;
  39     }
  40 
 
  41     @Override
  42     public boolean add(T e) {
  43         if (list.size() > max) {
 
  44             removeOldest();
  45         }
  46         // add object
  47         long timestamp = System.nanoTime();
 
  48         int insertionIdx = Collections.binarySearch(this, e, comparator);
  49         if (insertionIdx < 0) {// not found
 
  50             insertionIdx = (-insertionIdx) - 1;
  51             list.add(insertionIdx, new Pair<Long, T>(timestamp, e));
  52         } else {
 
  53             // found
  54             list.set(insertionIdx, new Pair<Long, T>(timestamp, e));
  55         }
 
  56 
  57         // as timestamps are sorted, we just remove the oldest (first)
  58         if (timestamps.size() > max) {
 
  59             timestamps.remove(0);
  60         }
  61         // update timestamp
  62         Pair<Long, Integer> t = new Pair<Long, Integer>(timestamp, insertionIdx);
 
  63         timestamps.add(t);
  64         return true;
  65     }
  66 
 
  67     @Override
  68     public void add(int index, T element) {
  69         throw new UnsupportedOperationException(
 
  70                 "can't add element at arbitrary index, must use add to keep sorted order");
  71     }
  72 
  73     @Override
 
  74     public boolean addAll(Collection<? extends T> c) {
  75         for (T e : c) {
 
  76             add(e);
  77         }
  78         return c.size() > 0;
  79     }
 
  80 
  81     @Override
  82     public boolean addAll(int index, Collection<? extends T> c) {
 
  83         throw new UnsupportedOperationException(
  84                 "can't add element at arbitrary index, must use addAll to keep sorted order");
  85     }
 
  86 
  87     @Override
  88     public void clear() {
  89         list.clear();
 
  90     }
  91 
  92     @SuppressWarnings("unchecked")
  93     @Override
 
  94     public boolean contains(Object e) {
  95         if (e == null) {
  96             return false;
 
  97         }
  98         try {
  99             return Collections.binarySearch(this, (T) e, comparator) >= 0;
 
 100         } catch (ClassCastException ex) {
 101             LOGGER.error("Unexpected type for contains "
 102                     + e.getClass().getName() + ": " + e);
 
 103             return false;
 104         }
 105     }
 106 
 107     @Override
 
 108     public boolean containsAll(Collection<?> c) {
 109         for (Object e : c) {
 110             if (!contains(e)) {
 
 111                 return false;
 112             }
 113         }
 114         return true;
 
 115     }
 116 
 117     @Override
 118     public T get(int index) {
 119         Pair<Long, T> e = list.get(index);
 
 120         return e != null ? e.getSecond() : null;
 121     }
 122 
 123     public T get(Object e) {
 
 124         int ndx = indexOf(e);
 125         if (ndx >= 0) {
 126             return get(ndx);
 127         }
 
 128         return null;
 129     }
 130 
 131     @SuppressWarnings("unchecked")
 132     @Override
 
 133     public int indexOf(Object e) {
 134         try {
 135             return Collections.binarySearch(this, (T) e, comparator);
 
 136         } catch (ClassCastException ex) {
 137             LOGGER.error("Unexpected type for get " + e.getClass().getName()
 138                     + ": " + e);
 
 139             return -1;
 140         }
 141     }
 142 
 143     @Override
 144     public boolean isEmpty() {
 
 145         return list.isEmpty();
 146     }
 147 
 148     @Override
 149     public Iterator<T> iterator() {
 
 150         final Iterator<Pair<Long, T>> it = list.iterator();
 151         return new Iterator<T>() {
 
 152 
 153             @Override
 154             public boolean hasNext() {
 155                 return it.hasNext();
 
 156             }
 157 
 158             @Override
 159             public T next() {
 160                 Pair<Long, T> e = it.next();
 
 161                 return e.getSecond();
 162             }
 163 
 164             @Override
 165             public void remove() {
 
 166                 it.remove();
 167             }
 168         };
 169     }
 170 
 171     @Override
 
 172     public int lastIndexOf(Object o) {
 173         for (int i = list.size() - 1; i >= 0; i--) {
 174             T e = get(i);
 
 175             if (e.equals(o)) {
 176                 return i;
 177             }
 178         }
 179         return -1;
 
 180     }
 181 
 182     @Override
 183     public ListIterator<T> listIterator() {
 184         final ListIterator<Pair<Long, T>> it = list.listIterator();
 
 185         return buildListIterator(it);
 186     }
 187 
 188     @Override
 189     public ListIterator<T> listIterator(int index) {
 
 190         final ListIterator<Pair<Long, T>> it = list.listIterator(index);
 191         return buildListIterator(it);
 192     }
 
 193 
 194     @SuppressWarnings("unchecked")
 195     @Override
 196     public boolean remove(Object e) {
 
 197         try {
 198             int ndx = Collections.binarySearch(this, (T) e, comparator);
 199             if (ndx >= 0) {
 
 200                 remove(ndx);
 201                 return true;
 202             } else {
 203                 return false;
 
 204             }
 205 
 206         } catch (ClassCastException ex) {
 207             LOGGER.error("Unexpected type for remove " + e.getClass().getName()
 
 208                     + ": " + e);
 209             return false;
 210         }
 211     }
 
 212 
 213     @Override
 214     public T remove(int index) {
 215         Pair<Long, T> e = list.remove(index);
 
 216         Pair<Long, Integer> t = new Pair<Long, Integer>(e.getFirst(), 0);
 217 
 218         int insertionIdx = Collections.binarySearch(timestamps, t, CMP);
 
 219         if (insertionIdx >= 0) {
 220             timestamps.remove(insertionIdx);
 221         }
 222         return e != null ? e.getSecond() : null;
 
 223     }
 224 
 225     @Override
 226     public boolean removeAll(Collection<?> c) {
 
 227         boolean all = true;
 228         for (Object e : c) {
 229             all = all && remove(e);
 
 230         }
 231         return all;
 232     }
 233 
 234     @Override
 235     public boolean retainAll(Collection<?> c) {
 
 236         boolean changed = false;
 237         Iterator<?> it = c.iterator();
 238         while (it.hasNext()) {
 
 239             Object e = it.next();
 240             if (!contains(e)) {
 241                 it.remove();
 242                 changed = true;
 243             }
 
 244         }
 245         return changed;
 246     }
 247 
 248     @Override
 
 249     public T set(int index, T element) {
 250         throw new UnsupportedOperationException();
 251     }
 
 252 
 253     @Override
 254     public int size() {
 255         return list.size();
 
 256     }
 257 
 258     @Override
 259     public List<T> subList(int fromIndex, int toIndex) {
 
 260         List<T> tlist = new ArrayList<T>();
 261         List<Pair<Long, T>> plist = list.subList(fromIndex, toIndex);
 
 262         for (Pair<Long, T> e : plist) {
 263             tlist.add(e.getSecond());
 264         }
 265         return tlist;
 
 266     }
 267 
 268     @Override
 269     public Object[] toArray() {
 270         return subList(0, list.size()).toArray();
 
 271     }
 272 
 273     @SuppressWarnings("hiding")
 274     @Override
 275     public <T> T[] toArray(T[] a) {
 
 276         return subList(0, list.size()).toArray(a);
 277     }
 278 
 279     @Override
 280     public String toString() {
 
 281         StringBuilder sb = new StringBuilder();
 282         Iterator<T> it = iterator();
 283         while (it.hasNext()) {
 
 284             sb.append(it.next() + ", ");
 285         }
 286         return sb.toString();
 287     }
 288 
 
 289     private void removeOldest() {
 290         timestamps.remove(timestamps.size() - 1);
 291     }
 292 
 293     private ListIterator<T> buildListIterator(
 
 294             final ListIterator<Pair<Long, T>> it) {
 295         return new ListIterator<T>() {
 
 296 
 297             @Override
 298             public void add(T e) {
 299                 it.add(new Pair<Long, T>(System.nanoTime(), e));
 
 300             }
 301 
 302             @Override
 303             public boolean hasNext() {
 304                 return it.hasNext();
 
 305 
 306             }
 307 
 308             @Override
 309             public boolean hasPrevious() {
 
 310                 return it.hasPrevious();
 311 
 312             }
 313 
 314             @Override
 315             public T next() {
 
 316                 Pair<Long, T> e = it.next();
 317                 return e.getSecond();
 318             }
 319 
 320             @Override
 
 321             public int nextIndex() {
 322                 return it.nextIndex();
 323 
 324             }
 
 325 
 326             @Override
 327             public T previous() {
 328                 Pair<Long, T> e = it.previous();
 
 329                 return e.getSecond();
 330             }
 331 
 332             @Override
 333             public int previousIndex() {
 
 334                 return it.previousIndex();
 335 
 336             }
 337 
 338             @Override
 339             public void remove() {
 
 340                 it.remove();
 341 
 342             }
 343 
 344             @Override
 345             public void set(T e) {
 
 346                 it.set(new Pair<Long, T>(System.nanoTime(), e));
 347 
 348             }
 349         };
 350     }
 
 351 
 352 }
 353 
 354 
 

Listing of LRUSortedList.java

   1 package com.plexobject.jmx.impl;
 
   2 
   3 import java.beans.PropertyChangeListener;
   4 import java.beans.PropertyChangeSupport;
   5 import java.util.Map;
 
   6 import java.util.concurrent.ConcurrentHashMap;
   7 import java.util.concurrent.atomic.AtomicLong;
   8 
   9 import javax.management.AttributeChangeNotification;
 
  10 import javax.management.MBeanNotificationInfo;
  11 import javax.management.Notification;
  12 import javax.management.NotificationBroadcasterSupport;
 
  13 import javax.management.NotificationListener;
  14 
  15 import org.apache.commons.lang.builder.EqualsBuilder;
  16 import org.apache.commons.lang.builder.HashCodeBuilder;
 
  17 import org.apache.commons.lang.builder.ToStringBuilder;
  18 import org.apache.log4j.Logger;
  19 
  20 import com.plexobject.jmx.ServiceJMXBean;
 
  21 import com.plexobject.metrics.Metric;
  22 import com.plexobject.util.TimeUtils;
  23 
  24 public class ServiceJMXBeanImpl extends NotificationBroadcasterSupport
 
  25         implements ServiceJMXBean, NotificationListener {
  26     private static final Logger LOGGER = Logger
  27             .getLogger(ServiceJMXBeanImpl.class);
 
  28     private Map<String, String> properties = new ConcurrentHashMap<String, String>();
  29     private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
 
  30 
  31     private final String serviceName;
  32     private AtomicLong totalErrors;
 
  33     private AtomicLong totalRequests;
  34 
  35     private AtomicLong sequenceNumber;
  36     private String state;
 
  37 
  38     public ServiceJMXBeanImpl(final String serviceName) {
  39         this.serviceName = serviceName;
 
  40         this.totalErrors = new AtomicLong();
  41         this.totalRequests = new AtomicLong();
  42         this.sequenceNumber = new AtomicLong();
 
  43     }
  44 
  45     @Override
  46     public double getAverageElapsedTimeInNanoSecs() {
 
  47         return Metric.getMetric(getServiceName())
  48                 .getAverageDurationInNanoSecs();
  49     }
  50 
 
  51     public String getProperty(final String name) {
  52         return properties.get(name);
  53     }
 
  54 
  55     public void setProperty(final String name, final String value) {
 
  56         final String oldValue = properties.put(name, value);
  57         final Notification notification = new AttributeChangeNotification(this,
 
  58                 sequenceNumber.incrementAndGet(), TimeUtils
  59                         .getCurrentTimeMillis(), name + " changed", name,
  60                 "String", oldValue, value);
  61         sendNotification(notification);
 
  62         handleNotification(notification, null);
  63     }
  64 
  65     @Override
 
  66     public String getServiceName() {
  67         return serviceName;
  68     }
  69 
 
  70     @Override
  71     public long getTotalDurationInNanoSecs() {
  72         return Metric.getMetric(getServiceName()).getTotalDurationInNanoSecs();
 
  73     }
  74 
  75     @Override
  76     public long getTotalErrors() {
 
  77         return totalErrors.get();
  78     }
  79 
  80     public void incrementError() {
 
  81         final long oldErrors = totalErrors.getAndIncrement();
  82         final Notification notification = new AttributeChangeNotification(this,
 
  83                 sequenceNumber.incrementAndGet(), TimeUtils
  84                         .getCurrentTimeMillis(), "Errors changed", "Errors",
  85                 "long", oldErrors, oldErrors + 1);
 
  86         sendNotification(notification);
  87     }
  88 
  89     @Override
  90     public long getTotalRequests() {
 
  91         return totalRequests.get();
  92     }
  93 
  94     public void incrementRequests() {
 
  95         final long oldRequests = totalRequests.getAndIncrement();
  96         final Notification notification = new AttributeChangeNotification(this,
 
  97                 sequenceNumber.incrementAndGet(), TimeUtils
  98                         .getCurrentTimeMillis(), "Requests changed",
  99                 "Requests", "long", oldRequests, oldRequests + 1);
 
 100         sendNotification(notification);
 101     }
 102 
 103     @Override
 104     public MBeanNotificationInfo[] getNotificationInfo() {
 105         String[] types = new String[] { AttributeChangeNotification.ATTRIBUTE_CHANGE };
 
 106         String name = AttributeChangeNotification.class.getName();
 107         String description = "An attribute of this MBean has changed";
 108         MBeanNotificationInfo info = new MBeanNotificationInfo(types, name,
 
 109                 description);
 110 
 111         return new MBeanNotificationInfo[] { info };
 112     }
 113 
 
 114     @Override
 115     public String getState() {
 116         return state;
 117     }
 118 
 
 119     /**
 120      * @param state
 121      *            the state to set
 
 122      */
 123     public void setState(String state) {
 124         this.state = state;
 125     }
 
 126 
 127     /**
 128      * @see java.lang.Object#equals(Object)
 
 129      */
 130     @Override
 131     public boolean equals(Object object) {
 132         if (!(object instanceof ServiceJMXBeanImpl)) {
 
 133             return false;
 134         }
 135         ServiceJMXBeanImpl rhs = (ServiceJMXBeanImpl) object;
 136         return new EqualsBuilder().append(this.serviceName, rhs.serviceName)
 
 137                 .isEquals();
 138     }
 139 
 140     /**
 141      * @see java.lang.Object#hashCode()
 
 142      */
 143     @Override
 144     public int hashCode() {
 145         return new HashCodeBuilder(786529047, 1924536713).append(
 
 146                 this.serviceName).toHashCode();
 147     }
 148 
 149     /**
 150      * @see java.lang.Object#toString()
 
 151      */
 152     @Override
 153     public String toString() {
 154         return new ToStringBuilder(this)
 
 155                 .append("serviceName", this.serviceName).append("totalErrors",
 156                         this.totalErrors).append("totalRequests",
 157                         this.totalRequests).append("totalRequests",
 
 158                         this.totalRequests).append("state", this.state).append(
 159                         "properties", this.properties).toString();
 160     }
 
 161 
 162     public void addPropertyChangeListener(PropertyChangeListener pcl) {
 163         pcs.addPropertyChangeListener(pcl);
 164     }
 165 
 
 166     public void removePropertyChangeListener(PropertyChangeListener pcl) {
 167         pcs.removePropertyChangeListener(pcl);
 168 
 169     }
 170 
 
 171     @Override
 172     public void handleNotification(Notification notification, Object handback) {
 173         LOGGER.info("Received notification: ClassName: "
 174                 + notification.getClass().getName() + ", Source: "
 
 175                 + notification.getSource() + ", Type: "
 176                 + notification.getType() + ", tMessage: "
 177                 + notification.getMessage());
 178         if (notification instanceof AttributeChangeNotification) {
 
 179             AttributeChangeNotification acn = (AttributeChangeNotification) notification;
 180             pcs.firePropertyChange(acn.getAttributeName(), acn.getOldValue(),
 181                     acn.getNewValue());
 182 
 183         }
 184     }
 
 185 }
 186 
 187 
 

Testing

Finally, here is how you can test this filter:

  1 package com.plexobject;
 
  2 
  3 import java.net.InetAddress;
  4 import java.util.Date;
  5 
 
  6 import org.apache.log4j.Logger;
  7 import org.apache.log4j.PatternLayout;
  8 import org.apache.log4j.net.SMTPAppender;
 
  9 
 10 import com.plexobject.log.FilteredSMTPAppender;
 11 
 12 public class Main {
 
 13     private static final Logger LOGGER = Logger.getLogger(Main.class);
 14     public static void main(String[] args) {
 
 15         SMTPAppender appender = new FilteredSMTPAppender();
 16         try {
 17             appender.setTo("bhatti@xxx.com");
 18             appender.setFrom("bhatti@xxx.com");
 
 19             appender.setSMTPHost("smtp.xxx.net");
 20             appender.setLocationInfo(true);
 21             appender.setSubject("Error from " + InetAddress.getLocalHost());
 22 
 
 23             appender.setLayout(new PatternLayout());
 24             appender.activateOptions();
 25             LOGGER.addAppender(appender);
 26         } catch (Exception e) {
 
 27             LOGGER.error("Failed to register smtp appender", e);
 28         }
 29         while (true) {
 30             try {
 
 31                 throw new Exception("throwing exception at " + new Date());
 32             } catch (Exception e) {
 
 33                 LOGGER.error("Logging error at " + new Date(), e);
 34             }
 35             try {
 
 36                 Thread.sleep(1000);
 37             } catch (InterruptedException e) {
 38                 Thread.interrupted();
 39             }
 40         }
 
 41     }
 42 }
 43 
 44 
 

Above code simulates error generation every second, but it sends email based on the throttling level defined in the configuration. Obviously you can use log4j properties file to define all this configuration, e.g.

<!– Send email when error happens –>
<appender name=”APP-EMAIL” class=”com.plexobject.log.FilteredSMTPAppender”>
<param name=”BufferSize” value=”256″ />
<param name=”SMTPHost” value=”smtp.xxx.net” />
<param name=”From” value=”bhatti@xxx.com” />
<param name=”To” value=”bhatti@xxx.com” />
<param name=”Subject” value=”Production Error” />
<layout class=”org.apache.log4j.PatternLayout”>
<param name=”ConversionPattern”
value=”[%d{ISO8601}]%n%n%-5p%n%n%c%n%n%m%n%n” />
</layout>

<filter class=”org.apache.log4j.varia.StringMatchFilter”>
<param name=”StringToMatch” value=”My Error”/>
<param name=”AcceptOnMatch” value=”false” />
</filter>
</appender>

Summary

I am skipping other classes, but you can download entire code from FilteredSMTPAppender.zip. This solution seems to be working from me but feel free to share your experience with similar problems.

No Comments »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment

You must be logged in to post a comment.

Powered by WordPress