I am using serilog in my C# .NET 8 Worker Service project. I want a special behavior and have written a sink to filter the logging.
In my Program.cs
file, I call the following function to create my logger:
// Method to configure Serilog with DuplicateFilterSink
Serilog.ILogger CreateSerilogLogger()
{
LoggerConfiguration loggerConfig = new LoggerConfiguration()
.WriteTo.Console() // for debugging
.WriteTo.File(
path: Settings.Default.LogFileSpec, // base file name
rollingInterval: RollingInterval.Day, // automatically creates a new file daily
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
);
// Add Event Log only for Windows
if (OperatingSystem.IsWindows())
{
loggerConfig.WriteTo.EventLog(
source: Settings.Default.EventLogSource,
logName: Settings.Default.EventLogName,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Warning
);
}
DuplicateFilterSink duplicateFilterSink = new DuplicateFilterSink(loggerConfig.CreateLogger());
return new LoggerConfiguration()
.WriteTo.Sink(duplicateFilterSink)
.CreateLogger();
}
My DuplicateFilterSink
class looks like this:
public class DuplicateFilterSink : ILogEventSink
{
private readonly ILogEventSink _innerSink;
private LogEvent? _lastLogEvent;
private int _duplicateCount = 0;
public DuplicateFilterSink(ILogEventSink innerSink)
{
_innerSink = innerSink;
}
public void Emit(LogEvent logEvent)
{
// Check if the message is the same as the previous one
if (_lastLogEvent != null && logEvent.MessageTemplate.Text == _lastLogEvent.MessageTemplate.Text)
{
_duplicateCount++;
}
else
{
// If the message has changed, log the previous duplicate count
if (_duplicateCount > 0)
{
List<LogEventProperty> properties = new List<LogEventProperty>();
foreach (var thing in logEvent.Properties)
{
LogEventProperty property = new LogEventProperty(thing.Key, thing.Value);
properties.Add(property);
}
string dupmsg = $"The previous message occurred {_duplicateCount} times";
MessageTemplate duplicateTemplate = new MessageTemplate(dupmsg, Array.Empty<MessageTemplateToken>());
LogEvent duplicateMessage = new LogEvent(
logEvent.Timestamp,
logEvent.Level,
logEvent.Exception,
duplicateTemplate,
properties.ToArray()
);
_innerSink.Emit(duplicateMessage);
_duplicateCount = 0;
}
// Log the new message
_innerSink.Emit(logEvent);
_lastLogEvent = logEvent;
}
}
}
The idea is that if the LogEvent
matches the previous LogEvent
, it will not be emitted but only counted. As soon as the LogEvent
DOESN'T match the previous LogEvent
AND the count is greater than zero, it will emit a message saying the previous message occurred X times and then it would emit the new message. This will prevent runaway messages in a tight loop from filling up my log files.
This all works except when the duplicateMessage
LogEvent
is emitted, the text is blank. I believe it is because I am using an empty MessageTemplateToken
array. That is the only difference between the duplicatMessage
LogEvent
and the LogEvent
that is the parameter.
If this is the problem, I don't know how to correct it. The MessageTemplateToken
is an abstract class so I can't instantiate it and I can't find any class that derives from it. I also can't use the existing MessageTemplateTokens
from the LogEvent
parameter's MessageTemplate
because I would have to change the text inside it and the Text property is read-only.
Question #1: Is this my problem? Is the blank
MessageTemplateToken
array causing my emittedLogEvent
to write a blank line?Question #2: is there any way to create a valid
MessageTemplate
with a validMessageTemplateToken
array?
I am using serilog in my C# .NET 8 Worker Service project. I want a special behavior and have written a sink to filter the logging.
In my Program.cs
file, I call the following function to create my logger:
// Method to configure Serilog with DuplicateFilterSink
Serilog.ILogger CreateSerilogLogger()
{
LoggerConfiguration loggerConfig = new LoggerConfiguration()
.WriteTo.Console() // for debugging
.WriteTo.File(
path: Settings.Default.LogFileSpec, // base file name
rollingInterval: RollingInterval.Day, // automatically creates a new file daily
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
);
// Add Event Log only for Windows
if (OperatingSystem.IsWindows())
{
loggerConfig.WriteTo.EventLog(
source: Settings.Default.EventLogSource,
logName: Settings.Default.EventLogName,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Warning
);
}
DuplicateFilterSink duplicateFilterSink = new DuplicateFilterSink(loggerConfig.CreateLogger());
return new LoggerConfiguration()
.WriteTo.Sink(duplicateFilterSink)
.CreateLogger();
}
My DuplicateFilterSink
class looks like this:
public class DuplicateFilterSink : ILogEventSink
{
private readonly ILogEventSink _innerSink;
private LogEvent? _lastLogEvent;
private int _duplicateCount = 0;
public DuplicateFilterSink(ILogEventSink innerSink)
{
_innerSink = innerSink;
}
public void Emit(LogEvent logEvent)
{
// Check if the message is the same as the previous one
if (_lastLogEvent != null && logEvent.MessageTemplate.Text == _lastLogEvent.MessageTemplate.Text)
{
_duplicateCount++;
}
else
{
// If the message has changed, log the previous duplicate count
if (_duplicateCount > 0)
{
List<LogEventProperty> properties = new List<LogEventProperty>();
foreach (var thing in logEvent.Properties)
{
LogEventProperty property = new LogEventProperty(thing.Key, thing.Value);
properties.Add(property);
}
string dupmsg = $"The previous message occurred {_duplicateCount} times";
MessageTemplate duplicateTemplate = new MessageTemplate(dupmsg, Array.Empty<MessageTemplateToken>());
LogEvent duplicateMessage = new LogEvent(
logEvent.Timestamp,
logEvent.Level,
logEvent.Exception,
duplicateTemplate,
properties.ToArray()
);
_innerSink.Emit(duplicateMessage);
_duplicateCount = 0;
}
// Log the new message
_innerSink.Emit(logEvent);
_lastLogEvent = logEvent;
}
}
}
The idea is that if the LogEvent
matches the previous LogEvent
, it will not be emitted but only counted. As soon as the LogEvent
DOESN'T match the previous LogEvent
AND the count is greater than zero, it will emit a message saying the previous message occurred X times and then it would emit the new message. This will prevent runaway messages in a tight loop from filling up my log files.
This all works except when the duplicateMessage
LogEvent
is emitted, the text is blank. I believe it is because I am using an empty MessageTemplateToken
array. That is the only difference between the duplicatMessage
LogEvent
and the LogEvent
that is the parameter.
If this is the problem, I don't know how to correct it. The MessageTemplateToken
is an abstract class so I can't instantiate it and I can't find any class that derives from it. I also can't use the existing MessageTemplateTokens
from the LogEvent
parameter's MessageTemplate
because I would have to change the text inside it and the Text property is read-only.
Question #1: Is this my problem? Is the blank
MessageTemplateToken
array causing my emittedLogEvent
to write a blank line?Question #2: is there any way to create a valid
MessageTemplate
with a validMessageTemplateToken
array?
- shouldn't you be writing to _lastLogEvent instead of logEvent? – jdweng Commented Feb 5 at 22:20
- Jdweng- thanks for replying. I'm not sure what you're asking. I set _lastLogEvent = logEvent just before the end of the else clause. I'm not writing to logEvent at all. Please clarify. – Dogulas Commented Feb 6 at 13:45
- marc_s - my god! was all that editing just to build up points? – Dogulas Commented Feb 6 at 16:18
3 Answers
Reset to default 1It's possible to just construct message template tokens, e.g.:
var template = new MessageTemplate(new[] { new TextToken("This is a message") });
For templates with structured data you might want ILogger.BindMessageTemplate()
:
Log.Logger.BindMessageTemplate(
"The previous message occurred {DuplicateCount} times",
new object[] { _duplicateCount },
out var template,
out var properties);
The output template
and properties
values are useful when constructing a log event.
Ok, I fixed this myself with a really really ugly hack.
My DuplicateFilterSink class now looks like this:
public class DuplicateFilterSink : ILogEventSink
{
private readonly ILogEventSink _innerSink;
private LogEvent? _prevLogEvent;
private int _duplicateCount = 0;
public DuplicateFilterSink(ILogEventSink innerSink)
{
_innerSink = innerSink;
}
public void Emit(LogEvent logEvent)
{
// Check if the message is the same as the previous one
if (_prevLogEvent != null && logEvent.MessageTemplate.Text == _prevLogEvent.MessageTemplate.Text)
{
_duplicateCount++;
}
else
{
// If the message has changed, log the previous duplicate count
if (_duplicateCount > 0 && !logEvent.MessageTemplate.Text.StartsWith(" * The previous message occurred"))
{
string dupmsg = $" * The previous message occurred {_duplicateCount} times";
Log.Information(dupmsg, _duplicateCount);
_duplicateCount = 0;
}
// Log the new message
_innerSink.Emit(logEvent);
_prevLogEvent = logEvent;
}
}
}
Since I have not figured out a way to create a valid MessageTemplate with a valid MessageTemplateToken, I tried using the Log.Information() line, but that created an infinite recursion loop because the Log method kept calling the Sink which called the Log method which called the... well, you get it.
I combated this problem by adding the "&& !logEvent.MessageTemplate.Text.StartsWith(..." condition to the if statement so that the second time through it would not Log method again.
This works, but is horribly Kludgy. I will be greatful if anyone can solve my original problem in a "best-practices" way.
For the sake of completeness and with the advise from Nicholas this is a snippet from my Program.cs file. Remeber that this is a .NET 8 Worker Service project.
// Method to configure Serilog with DuplicateFilterSink
Serilog.ILogger CreateSerilogLogger()
{
LoggerConfiguration loggerConfig = new LoggerConfiguration()
.WriteTo.Console() // for debugging
.WriteTo.File(
path: Settings.Default.LogFileSpec, // base file name
rollingInterval: RollingInterval.Day, // automatically creates a new file daily
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
);
// Add Event Log only for Windows
if (OperatingSystem.IsWindows())
{
loggerConfig.WriteTo.EventLog(
source: Settings.Default.EventLogSource,
logName: Settings.Default.EventLogName,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Warning
);
}
DuplicateFilterSink duplicateFilterSink = new DuplicateFilterSink(loggerConfig.CreateLogger());
return new LoggerConfiguration()
.WriteTo.Sink(duplicateFilterSink)
.CreateLogger();
}
And this is my DuplicateFilterSink class.
public class DuplicateFilterSink : ILogEventSink
{
private readonly ILogEventSink _innerSink;
private LogEvent? _prevLogEvent;
private int _duplicateCount = 0;
public DuplicateFilterSink(ILogEventSink innerSink)
{
_innerSink = innerSink;
}
public void Emit(LogEvent logEvent)
{
// Check if the message is the same as the previous one
if (_prevLogEvent != null && logEvent.MessageTemplate.Text == _prevLogEvent.MessageTemplate.Text)
{
_duplicateCount++;
}
else
{
// If the message has changed, log the previous duplicate count
if (_duplicateCount > 0)
{
// copy the new log event properties to use with the duplicate message
List<LogEventProperty> properties = new List<LogEventProperty>();
foreach (KeyValuePair<string, LogEventPropertyValue> keyValPair in logEvent.Properties)
{
LogEventProperty property = new LogEventProperty(keyValPair.Key, keyValPair.Value);
properties.Add(property);
}
// create the text template for the duplicate message
string dupmsg = $" * The previous message occurred {_duplicateCount} times";
MessageTemplate duplicateTemplate = new MessageTemplate(new[] { new TextToken(dupmsg) });
// create a log event for the duplicate message
LogEvent dupMsgLogEvent = new LogEvent(
logEvent.Timestamp,
logEvent.Level,
logEvent.Exception,
duplicateTemplate,
properties.ToArray()
);
// log the duplicate message
_innerSink.Emit(dupMsgLogEvent);
// reset the counter
_duplicateCount = 0;
}
// Log the new message
_innerSink.Emit(logEvent);
// save the new message as the prev message for next time
_prevLogEvent = logEvent;
}
}
}
If you want to add the functionality to filter out duplicate messages, feel free to use this as a guide.