log4net smtp caching appender or how to not get 10,000 emails

This post begins with thanks to a post on www.l4ndash.com by Ken Parrish and a few other posts I found online.

I began using Ken’s code in production but found that it didn’t always reliably fire for me. I found that even though I had a flush interval of 5 minutes, I might get a log email with 10 errors and the ninth error could be two hours before the tenth. It seems that the logger would only fire when a larger event fired on the thread I was running. (The project is a window service).

After pulling down the log4net source code, I found that somehow I didn’t have the state just right when I called Flush on the BufferingAppenderSkeleton.

Being lazy, I ended up modifying Ken’s code to skip that flush all together instead of figuring out how to get that state right. It had something do to with a lossybuffer setting I didn’t care about.

Here is the code I ended up with:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using log4net.Appender;
using log4net.Core;

namespace log4net.Extensions
{
public class SmtpCachingAppender : SmtpAppender
{
private readonly List<LoggingEvent> _loggingEvents = new List<LoggingEvent>();

private int _numberOfCachedMessages;
private bool _timeToFlushHasElapsed;
private Timer _timer;

public SmtpCachingAppender()
{
FlushIntervalTime = new TimeSpan(0, 5, 0);
MinNumberToCacheBeforeSending = 20;
}

public TimeSpan FlushIntervalTime { get; set; }

public int MinNumberToCacheBeforeSending { get; set; }

public int MaxBufferSize { get; set; }

public override void ActivateOptions()
{
if (FlushIntervalTime > TimeSpan.Zero)
{
_timer = new Timer(OnTimer, null, FlushIntervalTime, FlushIntervalTime);
}
base.ActivateOptions();
}

private void OnTimer(Object stateInfo)
{
_timeToFlushHasElapsed = true;
SendBuffer();
}

private void SendBuffer()
{
try
{
if (MaxBufferSize != 0)
{
int numRemoved = _loggingEvents.Count – MaxBufferSize;
if ((numRemoved > 0) && (numRemoved <= _loggingEvents.Count))
{
_loggingEvents.RemoveRange(0, numRemoved);
}
}

_numberOfCachedMessages++;

if (((MinNumberToCacheBeforeSending != 0) && (_numberOfCachedMessages >= MinNumberToCacheBeforeSending)) || _timeToFlushHasElapsed)
{
if (_loggingEvents.Count > 0)
{
LoggingEvent[] bufferedEvents = _loggingEvents.ToArray();

base.SendBuffer(bufferedEvents);

_loggingEvents.Clear();
}
// Reset cache buffer conditions.
_numberOfCachedMessages = 0;
_timeToFlushHasElapsed = false;
}
}
catch (Exception)

{
//We never want to crash so we let nothing happen here purposefully
}
}
protected override void SendBuffer(LoggingEvent[] events)
{
foreach (var loggingEvent in events.Where(loggingEvent => !_loggingEvents.Contains(loggingEvent)))
{
_loggingEvents.Add(loggingEvent);
}
SendBuffer();
}
}
}

My config entry looks like this:

<appender name=”LimitedSmtpAppender” type=”log4net.Extensions.SmtpCachingAppender”>
<threshold value=”WARN”/>
<to value=”blah@blah.com”/>
<from value=”blah@blah.com”/>
<subject value=”ERROR: This was bad on this server”/>
<IncludeContextLogEvents value=”false”/>
<smtpHost value=”1.2.3.4″/>
<bufferSize value=”0″/>
<lossy value=”false”/>
<priority value=”high”/>
<FlushIntervalTime value=”00:00:30″/>
<MinNumberToCacheBeforeSending value=”10″/>
<MaxBufferSize value=”0″/><!– USE WITH CARE setting this to something other than 1, CAN make you lose messages permanently…–>
<layout type=”log4net.Layout.PatternLayout”>
<param name=”ConversionPattern” value=”%date{yyyy-MM-dd HH:mm:ss.fff} [%-2thread] [%-5level] [%logger] %message%newline%exception”/>
</layout>
</appender>

Happy logging,

-Jim

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.