Logback SMTPAppender send only one email at a particular time with all the exceptions

3.9k views Asked by At

Is there a way to configure the SMTPAppender in LogBack to meet the following criteria?

  1. Group all the exceptions into one message
  2. Only send the daily log report if exceptions occurred
  3. Send the report only once, grouped in one email, at a particular time of the day.

My current implementation is far from doing the above, but currently it sends 3 emails when an exception occurs - the exception message, the stacktrace, and a flush of the buffer.

<!-- Filter duplicate Log Messages - Very important for Email Reports -->
<turboFilter class="ch.qos.logback.classic.turbo.DuplicateMessageFilter">
    <AllowedRepetitions>1</AllowedRepetitions>
    <CacheSize>1000</CacheSize>
</turboFilter>

<!--
    ############################################################
            BASIC APPENDER
    ############################################################
-->

<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>%d{HH:mm:ss.SSS} %-55(%X{user} %level [%thread] %logger{20}) - %msg%n</pattern>
    </encoder>
</appender>


<!--
    ############################################################
            EMAIL APPENDER
    ############################################################
-->
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />

<appender name="Email" class="ch.qos.logback.classic.net.SMTPAppender">
    <smtpHost>SERVER</smtpHost>
    <smtpPort>PORT</smtpPort>
    <asynchronousSending>false</asynchronousSending>
    <from>SENDER</from>
    <to>RECIPIENT</to>
    <subject>SUBJECT</subject>

    <layout class="ch.qos.logback.classic.PatternLayout">
        <pattern>%d{HH:mm:ss.SSS} %-55(%X{user} %level [%thread] %logger{20}) - %msg%n</pattern>
    </layout>

</appender>

<!--
    ############################################################
            OTHER
    ############################################################
-->
<root level="INFO">
    <appender-ref ref="Console"/>
    <appender-ref ref="RollingFile"/>
    <appender-ref ref="Email"/>
</root>
1

There are 1 answers

4
assylias On BEST ANSWER

One simple solution is to log those errors to a file and have a script on your server/machine that reads the file once a day and sends an email.

If you want to use an appender, it seems to me that you would need to roll your own as I don't think the standard SMTPAppender enables you to send emails once a day:

  • extend SMTPAppender
  • override the sendBuffer method that is in SMTPAppenderBase so that it simply adds the log message to a collection
  • add a ScheduledExecutorService to your appender that runs a sendEmail method once a day
  • the sendEmail method would synchronize on this for thread safety, check if the collection is empty, send an email with all the errors and clear the collection

A basic implementation could look like the class below (I have not tested it - I'm using Java 8 syntax but you can replace it by anonymous classes if required). Note that I only keep the event that caused the exception, you may also want to keep the content of the CyclicBuffer in the sendBuffer method and/or add some error separators between errors in the sendEmail method. This can become quite complex and is to be fine-tuned depending on your requirements.

public class ScheduledSMTPAppender extends SMTPAppender {
  private final ThreadFactory tf = r -> {
    Thread t = new Thread(r, "ScheduledSMTPAppender Thread");
    t.setDaemon(true); //make daemon or it will prevent your program to exit
    return t;
  };
  private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, tf);
  private final List<ILoggingEvent> events = new ArrayList<> ();

  private int maxMessages = 10;

  public ScheduledSMTPAppender() { super(); }
  public ScheduledSMTPAppender(EventEvaluator<ILoggingEvent> eventEvaluator) { super(eventEvaluator); }

  @Override public void start() {
    super.start();
    scheduler.scheduleAtFixedRate(this::sendEmail, 1, 1, TimeUnit.DAYS);
  }

  @Override protected void sendBuffer(CyclicBuffer<ILoggingEvent> cb, ILoggingEvent lastEventObject) {
    events.add(lastEventObject);
    if (events.size() > maxMessages) sendEmail();
  }

  //needs to be synchronized for thread safety
  private synchronized void sendEmail() {
    try {
      if (events.isEmpty()) return;
      ILoggingEvent lastEvent = events.get(events.size() - 1);
      events.remove(events.size() - 1);
      CyclicBuffer<ILoggingEvent> cb;
      if (events.isEmpty()) {
        cb = new CyclicBuffer<>(1);
      } else {
        cb = new CyclicBuffer<>(events.size());
        for (ILoggingEvent e : events) cb.add(e);
      }
      super.sendBuffer(cb, lastEvent);
      events.clear();
    } catch (Exception e) {
      //Important to have a catch all here or the scheduled task will die
      addError("Error occurred while sending e-mail notification.", e);
    }
  }

  //this allows to make "maxMessages" a parameter of your appender
  public int getMaxMessages() { return maxMessages; }
  public void setMaxMessages(int maxMessages) { this.maxMessages = maxMessages; }
}

Your logback configuration file then looks like:

<appender name="Email" class="your.package.ScheduledSMTPAppender">
    <smtpHost>SERVER</smtpHost>
    <smtpPort>PORT</smtpPort>
    <asynchronousSending>false</asynchronousSending>
    <from>SENDER</from>
    <to>RECIPIENT</to>
    <subject>SUBJECT</subject>
    <maxMessages>10</maxMessages>

    <layout class="ch.qos.logback.classic.PatternLayout">
        <pattern>%d{HH:mm:ss.SSS} %-55(%X{user} %level [%thread] %logger{20}) - %msg%n</pattern>
    </layout>
</appender>

To go further, you could add parameters, such as the time of the day when this is sent, the number of emails per day etc.