I think it would be better if SMTPAppender itself has the feature described below
though I could implement the subclass of SMTPAppender having the feature.

Could anyone please think about integrating the changes to the original source?

If you have any questions, please send me e-mail.
Thanks very much.


*Added Feature

SMTPAppender doesn't send any e-mail in the specified interval
after the latest sending if the buffer is not full
but puts logging events into the buffer only.


*Why I Need The Feature

You may receive 20 e-mails in 10 seconds and
each mail may contain only 1 logging event.

For example, fatal errors may keep being logged
unless the administrator does something,
You receive many e-mails containg only 1 same logging event.
Basically I think one e-mail per 20 or 30 minutes is enough.

You may think we can write the custom class of TriggeringEventEvaluator
to avoid it. But it's not enough.
The current SMTPAppender can send e-mails
only when append(LoggingEvent) is invoked.
So if TriggeringEventEvaluator decides not to send an e-mail now,
the logging event is not sent until the next logging event.
It may be long time like 20 or 30 minutes later
and the e-mail may contain the logging event
the administrator has to do something urgently.


*Source Code Modification

org/apache/log4j/net/SMTPAppender.java

See below for diff. See the attached file for modified source.

54a55
>   private int interval;
58a60,62
>   protected Scheduler scheduler;
>   protected long sentTime;
>   protected long timeout;
166a171,173
>     scheduler = new Scheduler();
>     scheduler.setDaemon(true);
>     scheduler.start();
264d270
<   synchronized
267c273,299
<     this.closed = true;
---
>     synchronized(this) {
>       if (closed) {
>       return;
>       }
>       closed = true;
>       if (cb.length() > 0) {
>       sendMail();
>         // Or
>         // sentTime = 0; sendBuffer();
>         // for backwad compatibility
>         // because SMTPAppender may have subclasses
>         // and sendBuffer() may be overridden?
>       }
>     }
>     try {
>       scheduler.interrupt();
>     } catch(SecurityException e) {
>       LogLog.error("Got a SecurityException while interrupting for the "+
>                  "scheduler to finish.", e);
>     }
>     try {
>       scheduler.join();
>     } catch(InterruptedException e) {
>       LogLog.error("Got an InterruptedException while waiting for the "+
>                  "scheduler to finish.", e);
>     }
>     scheduler = null;
306a339,356
>   protected
>   void sendBuffer() {
>     try {
>       if (cb.length() >= cb.getMaxSize()) {
>       if (timeout > 0) {      // scheduler is waiting timeout.
>         scheduler.interrupt();
>       }
>       sendMail();
>       } else {
>       if (timeout == 0) {     // scheduler is waiting to be scheduled.
>         scheduler.interrupt();
>       }
>       }
>     } catch (Exception e) {
>       LogLog.error("Error occured while sending e-mail notification.", e);
>     }
>   }
>
311c361
<   void sendBuffer() {
---
>   void sendMail() {
331a382
>             sbuf.append(Layout.LINE_SEP);
346a398,399
>
>       sentTime = System.currentTimeMillis();
513a567,602
>   }
>
>   public
>   void setInterval(int interval) {
>     this.interval = interval;
>   }
>
>   public
>   int getInterval() {
>     return interval;
>   }
>
>   protected class Scheduler extends Thread {
>
>     protected Scheduler() {
>     }
>
>     public void run() {
>       synchronized (SMTPAppender.this) {
>       while (!closed) {
>         timeout = sentTime + interval * 1000 -
>                                       System.currentTimeMillis();
>         if (cb.length() > 0 && timeout < 0) {
>           SMTPAppender.this.sendMail();
>         } else {
>           try {
>             if (cb.length() <= 0 || timeout < 0) {
>               timeout = 0;
>             }
>             SMTPAppender.this.wait(timeout);
>           } catch (InterruptedException e) {
>           }
>         }
>       }
>       }
>     }


*Test

I tested the modified code using a XML configuration file.
I put
 <param name="Interval" value="10"/>
with or without
 <param name="BufferSize" value="4"/>
then I ran the test program on jdk-1.3.1 with javamail-1.2 and jaf-1.0.1.
I think it worked fine.


Ceki, thank you for your reply of the e-mail, "I18n of SMTPAppender and FileAppender".
I'm happy of that.


Naozo
/*
 * Copyright (C) The Apache Software Foundation. All rights reserved.
 *
 * This software is published under the terms of the Apache Software
 * License version 1.1, a copy of which has been included with this
 * distribution in the LICENSE.APL file.  */

package org.apache.log4j.net;

import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Layout;
import org.apache.log4j.Priority;
import org.apache.log4j.helpers.CyclicBuffer;
import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.spi.ErrorCode;
import org.apache.log4j.spi.ErrorHandler;
import org.apache.log4j.spi.TriggeringEventEvaluator;
import java.util.Properties;
import java.util.Date;

import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.mail.Multipart;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.AddressException;
import javax.mail.internet.MimeUtility;

/**
   Send an e-mail when a specific logging event occurs, typically on
   errors or fatal errors.

   <p>The number of logging events delivered in this e-mail depend on
   the value of <b>BufferSize</b> option. The
   <code>SMTPAppender</code> keeps only the last
   <code>BufferSize</code> logging events in its cyclic buffer. This
   keeps memory requirements at a reasonable level while still
   delivering useful application context.
   
   @author Ceki G&uuml;lc&uuml;
   @since 1.0 */
public class SMTPAppender extends AppenderSkeleton {
  private String to;
  private String from;
  private String subject;
  private String smtpHost;
  private int bufferSize = 512;
  private boolean locationInfo = false;
  private int interval;

  protected CyclicBuffer cb = new CyclicBuffer(bufferSize);
  protected Message msg;

  protected Scheduler scheduler;
  protected long sentTime;
  protected long timeout;


 /**
     A string constant used in naming the <em>To</em> field of
     outgoing e-mail output file. Current value of this string
     constant is <b>To</b>.

     <p>Note that all option keys are case sensitive.
     
     @deprecated Options are now handled using the JavaBeans paradigm. 
     This constant will be removed in the <em>near</em> term.
  */
  public static final String TO_OPTION = "To";

 /**
     A string constant used in naming the <em>From</em> field of
     outgoing e-mail output file. Current value of this string
     constant is <b>From</b>.

     <p>Note that all option keys are case sensitive.

     @deprecated Options are now handled using the JavaBeans paradigm. 
     This constant will be removed in the <em>near</em> term.     
  */
  public static final String FROM_OPTION = "From";

 /**
     A string constant used in naming the <em>Subject</em> field of
     outgoing e-mail output file. Current value of this string
     constant is <b>Subject</b>.

     <p>Note that all option keys are case sensitive.
     
     @deprecated Options are now handled using the JavaBeans paradigm. 
     This constant will be removed in the <em>near</em> term.
  */
  public static final String SUBJECT_OPTION = "Subject";


 /**
     A string constant used in naming the SMTP host that will be
     contacted to send the e-mail. Current value of this string
     constant is <b>SMTPHost</b>.

     <p>Note that all option keys are case sensitive.
     
     @deprecated Options are now handled using the JavaBeans paradigm. 
     This constant will be removed in the <em>near</em> term.
  */
  public static final String SMTP_HOST_OPTION = "SMTPHost";

 /**
     A string constant used in naming the cyclic buffer size option.
     Current value of this string constant is <b>BufferSize</b>.

     <p>Note that all option keys are case sensitive.
     
     @deprecated Options are now handled using the JavaBeans paradigm. 
     This constant will be removed in the <em>near</em> term.
  */
  public static final String BUFFER_SIZE_OPTION = "BufferSize";


 /**
     A string constant used in naming the class of the
     TriggeringEventEvaluator that this SMTPApepdner wll use. Current
     value of this string constant is <b>EvaluatorClass</b>.

     <p>Note that all option keys are case sensitive.
     
     @deprecated Options are now handled using the JavaBeans paradigm. 
     This constant will be removed in the <em>near</em> term.
  */
  public static final String EVALUATOR_CLASS_OPTION = "EvaluatorClass";


  /**
     A string constant used in naming the option for setting the the
     location information flag.  Current value of this string
     constant is <b>LocationInfo</b>.  

     <p>Note that all option keys are case sensitive.

     @deprecated Options are now handled using the JavaBeans paradigm. 
     This constant will be removed in the <em>near</em> term.
  */
  public static final String LOCATION_INFO_OPTION = "LocationInfo";
  
  protected TriggeringEventEvaluator evaluator;



  /**
     The default constructor will instantiate the appender with a
     {@link TriggeringEventEvaluator} that will trigger on events with
     priority ERROR or higher.*/
  public
  SMTPAppender() {
    this(new DefaultEvaluator());
  }

  
  /**
     Use <code>evaluator</code> passed as parameter as the {@link
     TriggeringEventEvaluator} for this SMTPAppender.  */
  public 
  SMTPAppender(TriggeringEventEvaluator evaluator) {
    this.evaluator = evaluator;
    scheduler = new Scheduler();
    scheduler.setDaemon(true);
    scheduler.start();
  }

 /**
     Retuns the option names for this component in addition in
     addition to the options of its super class {@link
     AppenderSkeleton}.
     
     @deprecated We now use JavaBeans introspection to configure
     components. Options strings are no longer needed.
  */
  public
  String[] getOptionStrings() {
    return OptionConverter.concatanateArrays(super.getOptionStrings(),
          new String[] {TO_OPTION, FROM_OPTION, SUBJECT_OPTION, 
			  SMTP_HOST_OPTION, BUFFER_SIZE_OPTION,  
			  EVALUATOR_CLASS_OPTION, LOCATION_INFO_OPTION });
  }
  

  /**
     Activate the specified options, such as the smtp host, the
     recipient, from, etc. */
  public
  void activateOptions() {
    Properties props = System.getProperties();
    if (smtpHost != null)
      props.put("mail.smtp.host", smtpHost);

    
    Session session = Session.getDefaultInstance(props, null);
    //session.setDebug(true);
    msg = new MimeMessage(session);
     
     try {
       if (from != null)
	 msg.setFrom(getAddress(from));
       else
	 msg.setFrom();

       msg.setRecipients(Message.RecipientType.TO, parseAddress(to));
       if(subject != null)
	 msg.setSubject(subject);
     } catch(MessagingException e) {
       LogLog.error("Could not activate SMTPAppender options.", e );
     }
  }
  
  /**
     Perform SMTPAppender specific appending actions, mainly adding
     the event to a cyclic buffer and checking if the event triggers
     an e-mail to be sent. */
  public
  void append(LoggingEvent event) {

    if(!checkEntryConditions()) {
      return;
    }

    event.getThreadName();
    event.getNDC();
    if(locationInfo) {
      event.getLocationInformation();	
    }
    cb.add(event);    
    if(evaluator.isTriggeringEvent(event)) {
      sendBuffer();
    }
  }

 /**
     This method determines if there is a sense in attempting to append.
     
     <p>It checks whether there is a set output target and also if
     there is a set layout. If these checks fail, then the boolean
     value <code>false</code> is returned. */
  protected
  boolean checkEntryConditions() {
    if(this.msg == null) {
      errorHandler.error("Message object not configured.");
      return false;
    }

    if(this.evaluator == null) {
      errorHandler.error("No TriggeringEventEvaluator is set for appender ["+
			 name+"].");
      return false;
    }

    
    if(this.layout == null) {
      errorHandler.error("No layout set for appender named ["+name+"].");
      return false;
    }
    return true;
  }


  public
  void close() {
    synchronized(this) {
      if (closed) {
	return; 
      }
      closed = true;
      if (cb.length() > 0) {
	sendMail();
        // Or
        // sentTime = 0; sendBuffer();
        // for backwad compatibility
        // because SMTPAppender may have subclasses
        // and sendBuffer() may be overridden?
      }
    }
    try {
      scheduler.interrupt();
    } catch(SecurityException e) {
      LogLog.error("Got a SecurityException while interrupting for the "+
		   "scheduler to finish.", e);
    }
    try {
      scheduler.join();
    } catch(InterruptedException e) {
      LogLog.error("Got an InterruptedException while waiting for the "+
		   "scheduler to finish.", e);
    }
    scheduler = null;
  }

  InternetAddress getAddress(String addressStr) {
    try {
      return new InternetAddress(addressStr);
    } catch(AddressException e) {
      errorHandler.error("Could not parse address ["+addressStr+"].", e,
			 ErrorCode.ADDRESS_PARSE_FAILURE);
      return null;
    }
  }

  InternetAddress[] parseAddress(String addressStr) {
    try {
      return InternetAddress.parse(addressStr, true);
    } catch(AddressException e) {
      errorHandler.error("Could not parse address ["+addressStr+"].", e,
			 ErrorCode.ADDRESS_PARSE_FAILURE);
      return null;
    }
  }

  /**
     Returns value of the <b>To</b> option.
   */
  public
  String getTo() {
    return to;
  }


  /**
     The <code>SMTPAppender</code> requires a {@link Layout layout}.  
  */
  public
  boolean requiresLayout() {
    return true;
  }

  protected
  void sendBuffer() {
    try {
      if (cb.length() >= cb.getMaxSize()) {
	if (timeout > 0) {	// scheduler is waiting timeout.
	  scheduler.interrupt();
	}
	sendMail();
      } else {
	if (timeout == 0) {	// scheduler is waiting to be scheduled.
	  scheduler.interrupt();
	}
      }
    } catch (Exception e) {
      LogLog.error("Error occured while sending e-mail notification.", e);
    }
  }

  /**
     Send the contents of the cyclic buffer as an e-mail message.
   */
  protected
  void sendMail() {

    // Note: this code already owns the monitor for this
    // appender. This frees us from needing to synchronize on 'cb'.
    try {      
      MimeBodyPart part = new MimeBodyPart();

      StringBuffer sbuf = new StringBuffer();
      String t = layout.getHeader();
      if(t != null)
	sbuf.append(t);
      int len =  cb.length(); 
      for(int i = 0; i < len; i++) {
	//sbuf.append(MimeUtility.encodeText(layout.format(cb.get())));
	LoggingEvent event = cb.get();
	sbuf.append(layout.format(event));
	if(layout.ignoresThrowable()) {
	  String[] s = event.getThrowableStrRep();
	  if (s != null) {
	    for(int j = 0; j < s.length; j++) {
	      sbuf.append(s[j]);
	      sbuf.append(Layout.LINE_SEP);
	    }
	  }
	}
      }
      t = layout.getFooter();
      if(t != null)
	sbuf.append(t);
      part.setContent(sbuf.toString(), layout.getContentType());      

      Multipart mp = new MimeMultipart();
      mp.addBodyPart(part);
      msg.setContent(mp);

      msg.setSentDate(new Date());
      Transport.send(msg);      

      sentTime = System.currentTimeMillis();
    } catch(Exception e) {
      LogLog.error("Error occured while sending e-mail notification.", e);
    }
  }
  


  /**
     Returns value of the <b>EvaluatorClass</b> option.
   */
  public
  String getEvaluatorClass() {
    return evaluator == null ? null : evaluator.getClass().getName();
  }
  
  /**
     Returns value of the <b>From</b> option.
   */
  public
  String getFrom() {
    return from;
  }

  /**
     Returns value of the <b>Subject</b> option.
   */
  public
  String getSubject() {
    return subject;
  }


/**
   @deprecated Use the setter method for the option directly, instead
   of the generic <code>setOption</code> method.  */
  public
  void setOption(String option, String value) {
    if(value == null) return;
    super.setOption(option, value);    

    if(option.equals(TO_OPTION)) 
      to = value;
    else if (option.equals(FROM_OPTION))
      from = value;
    else if (option.equals(SMTP_HOST_OPTION)) 
      smtpHost = value;
    else if (option.equals(SUBJECT_OPTION)) 
      subject = value;
    else if (option.equals(EVALUATOR_CLASS_OPTION)) {      
      evaluator = (TriggeringEventEvaluator) 
                OptionConverter.instantiateByClassName(value, 
                                           TriggeringEventEvaluator.class,
                                                       evaluator);    
    } else if (option.equals(BUFFER_SIZE_OPTION)) {
      bufferSize = OptionConverter.toInt(value, bufferSize);    
      cb.resize(bufferSize);
    } else if (option.equals(LOCATION_INFO_OPTION))
      locationInfo = OptionConverter.toBoolean(value, locationInfo);
  }


  /**
     The <b>From</b> option takes a string value which should be a
     e-mail address of the sender.
   */
  public
  void setFrom(String from) {
    this.from = from;
  }
  
  /**
     The <b>Subject</b> option takes a string value which should be a
     the subject of the e-mail message.
   */
  public
  void setSubject(String subject) {
    this.subject = subject;
  }
  

  /**
     The <b>BufferSize</b> option takes a positive integer
     representing the maximum number of logging events to collect in a
     cyclic buffer. When the <code>BufferSize</code> is reached,
     oldest events are deleted as new events are added to the
     buffer. By default the size of the cyclic buffer is 512 events.
   */
  public
  void setBufferSize(int bufferSize) {
    this.bufferSize = bufferSize;
    cb.resize(bufferSize);
  }
  
  /**
     The <b>SMTPHost</b> option takes a string value which should be a
     the host name of the SMTP server that will send the e-mail message.
   */
  public
  void setSMTPHost(String smtpHost) {
    this.smtpHost = smtpHost;
  }
  
  /**
     Returns value of the <b>SMTPHost</b> option.
   */
  public
  String getSMTPHost() {
    return smtpHost;
  }

  /**
     The <b>To</b> option takes a string value which should be a
     comma separated list of e-mail address of the recipients.
   */
  public
  void setTo(String to) {
    this.to = to;
  }

  

  /**
     Returns value of the <b>BufferSize</b> option.
   */
  public
  int getBufferSize() {
    return bufferSize;
  }
  
  /**
     The <b>EvaluatorClass</b> option takes a string value
     representing the name of the class implementing the {@link
     TriggeringEventEvaluator} interface. A corresponding object will
     be instantiated and assigned as the triggering event evaluator
     for the SMTPAppender.
   */
  public
  void setEvaluatorClass(String value) {
      evaluator = (TriggeringEventEvaluator) 
                OptionConverter.instantiateByClassName(value, 
					   TriggeringEventEvaluator.class,
						       evaluator);    
  }
  
  
  /**
     The <b>LocationInfo</b> option takes a boolean value. By
     default, it is set to false which means there will be no effort
     to extract the location information related to the event. As a
     result, the layout that formats the events as they are sent out
     in an e-mail is likely to place the wrong location information
     (if present in the format).
     
     <p>Location information extraction is comparatively very slow and
     should be avoided unless performance is not a concern.
   */
  public
  void setLocationInfo(boolean locationInfo) {
    this.locationInfo = locationInfo;
  }
  
  /**
     Returns value of the <b>LocationInfo</b> option.
   */
  public
  boolean getLocationInfo() {
    return locationInfo;
  }

  public
  void setInterval(int interval) {
    this.interval = interval;
  }

  public
  int getInterval() {
    return interval;
  }

  protected class Scheduler extends Thread {

    protected Scheduler() {
    }

    public void run() {
      synchronized (SMTPAppender.this) {
	while (!closed) {
	  timeout = sentTime + interval * 1000 -
					System.currentTimeMillis();
	  if (cb.length() > 0 && timeout < 0) {
	    SMTPAppender.this.sendMail();
	  } else {
	    try {
	      if (cb.length() <= 0 || timeout < 0) {
		timeout = 0;
	      }
	      SMTPAppender.this.wait(timeout);
	    } catch (InterruptedException e) {
	    }
	  }
	}
      }
    }
  }
}

class DefaultEvaluator implements TriggeringEventEvaluator {
  /**
     Is this <code>event</code> the e-mail triggering event?
     
     <p>This method returns <code>true</code>, if the event priority
     has ERROR priority or higher. Otherwise it returns
     <code>false</code>. */
  public 
  boolean isTriggeringEvent(LoggingEvent event) {
    return event.priority.isGreaterOrEqual(Priority.ERROR); 
  }
}

--
To unsubscribe, e-mail:   <mailto:[EMAIL PROTECTED]>
For additional commands, e-mail: <mailto:[EMAIL PROTECTED]>

Reply via email to