最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

How to create MessageTemplateTokens for Serilog LogEvents in C# .NET8 (Core) (in a Worker Service project) - Stack Overflow

programmeradmin1浏览0评论

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 emitted LogEvent to write a blank line?

  • Question #2: is there any way to create a valid MessageTemplate with a valid MessageTemplateToken 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 emitted LogEvent to write a blank line?

  • Question #2: is there any way to create a valid MessageTemplate with a valid MessageTemplateToken array?

Share Improve this question edited Feb 6 at 4:47 marc_s 755k184 gold badges1.4k silver badges1.5k bronze badges asked Feb 5 at 21:34 DogulasDogulas 477 bronze badges 3
  • 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
Add a comment  | 

3 Answers 3

Reset to default 1

It'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.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论