Archive for September 10th, 2008

A Grails-aware Log4j SMTP Appender

Wednesday, September 10th, 2008

Proper logging implementation is a complex issue. If you log too much in production you risk wasting CPU and I/O time. If you log too little, you won’t have enough information to diagnose problems. And no matter how efficiently you implement your logging, if you don’t check your logs you won’t know when bad things are happening.

So here’s a Log4j appender that sends emails for messages logged at the Error level or higher. Yes, Log4j comes with an appender that sends emails, org.apache.log4j.net.SMTPAppender. The one I present here is a little simpler, but more importantly it’s Grails-friendly. Specifically it doesn’t send emails unless the app is running in production mode. Error emails are great, but very annoying while debugging 🙂

import grails.util.GrailsUtil;

import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.spi.LoggingEvent;
import org.codehaus.groovy.grails.commons.GrailsApplication;

/**
 * Replacement for Log4j's SMTP appender that uses the mail service.
 *
 * @author Burt
 */
public class SmtpErrorAppender extends AppenderSkeleton {

  /**
   * Perform 1-time initialization.
   */
  public static void register() {
    if (!GrailsApplication.ENV_PRODUCTION.equals(GrailsUtil.getEnvironment())) {
      // only configure for production
      return;
    }

    SmtpErrorAppender appender = new SmtpErrorAppender();
    Logger.getRootLogger().addAppender(appender);
    Logger.getLogger("StackTrace").addAppender(appender);
  }

  /**
   * Constructor with default values.
   */
  public SmtpErrorAppender() {
    setThreshold(Level.ERROR);
    setLayout(new PatternLayout(
      "%-27d{dd/MMM/yyyy HH:mm:ss Z}%n%n%-5p%n%n%c%n%n%m%n%n"));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void append(final LoggingEvent event) {

    if (!event.getLevel().isGreaterOrEqual(Level.ERROR)) {
      return;
    }

    event.getThreadName();
    event.getNDC();
    event.getMDCCopy();

    mailService.sendErrorEmail(layout.format(event), event);
  }

  /**
   * {@inheritDoc}
   * @see org.apache.log4j.AppenderSkeleton#close()
   */
  public synchronized void close() {
    closed = true;
  }

  /**
   * {@inheritDoc}
   * @see org.apache.log4j.AppenderSkeleton#requiresLayout()
   */
  public boolean requiresLayout() {
    return true;
  }
}

Unfortunately since this class will be compiled with the application code after Config.groovy is loaded, you can’t add it to the Config.groovy log4j section. Instead, just put some initialization code in Bootstrap:

class BootStrap {

  def init = { servletContext ->
    // can't put this in Config.groovy since the class won't have been compiled yet
    SmtpErrorAppender.register()
  }
}

Also, note that I’ve omitted the setter and declaration of ‘mailService’, which you’ll need to provide. The Grails mail plugin is good candidate. The details of what the email body looks like and who receives the emails is left as an exercise for the reader.

As I said, this is different from the standard email appender, so use that one if you prefer, but either way you should be using something to automatically notify yourself when errors are happening.

One consideration that’s important when implementing this is clarifying what you consider an Error-level or Fatal message. Ideally a Fatal error message should be severe enough that pagers start going off and phones start ringing (that’ll take another appender). So it’s important to decide what gets logged and at what level, so you don’t overwhelm yourself and support people with so many non-critical messages that they start ignoring them.

Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 License.