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.