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