﻿#### 

#### Background

Our team had to integrate [SEQ logging](https://datalust.co/seq) on one of our Sitecore projects. We investigated a few articles on how to implement it and provided an initial solution.

The solution is based on wrapping the [Serilog SEQ logger](https://docs.datalust.co/docs/using-serilog) inside a [log4net Sitecore appender](https://sitecore-community.github.io/docs/documentation/Sitecore%20Fundamentals/Logging). Below, I'll show how it all worked.

But we faced a few issues:

- No exception stack trace was logged.
- The initial implementation was not optimized (poor performance).
- Context-based Serilog Enhancers were not working correctly.

After step-by-step fixing and analyzing the process, I decided to create a NuGet library to use Serilog together with Sitecore. This allows you to integrate it into your Sitecore solution and collect logging data in any available [Serilog Sinks](https://github.com/serilog/serilog/wiki/Provided-Sinks).

You can visit the following links to try it:

- [Source code](https://github.com/izharikov/SitecoreSerilog)
- [NuGet Package](https://www.nuget.org/packages/SitecoreSerilog)
- [Github Gist](https://gist.github.com/izharikov/42801610f971b8d7981d35f04f45083d) with all sources listed in this article

*Note*. You should also consider using [Seq.Client.Log4Net](https://github.com/datalust/seq-client-log4net) - an officially supported package for SEQ in log4net (but to integrate additional logging platforms, some other log4net package should be used as well).

#### Implementation

##### The Beginning

###### Source

We started with this [article](https://himadritechblog.wordpress.com/2020/11/14/serilog-appender-for-sitecore-logging) written by Himadri Chakrabarti - it's a really great starting point. Here is the source of the appender:

```
using log4net.spi;
using System;
using log4net.helpers;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using ILogger = Serilog.ILogger;

namespace log4net.Appender
{
    public class SerilogAppender : BufferingAppenderSkeleton
    {
        public string MinimumLevel { get; set; }

        public string ApiKey { get; set; }

        public string SeqHost { get; set; }

        [Obsolete("Use the BufferingAppenderSkeleton Fix methods")]
        public bool LocationInfo => false;

        protected override void SendBuffer(LoggingEvent[] events)
        {
            if (string.IsNullOrEmpty(SeqHost))
            {
                return;
            }

            using (
                var log = new LoggerConfiguration()
                    .MinimumLevel.ControlledBy(new LoggingLevelSwitch(GetLogEventLevel(MinimumLevel, LogEventLevel.Information)))
                    .Enrich.WithProperty("ApplicationName", GetSite())
                    .Enrich.WithProperty("SpokeName", GetRoleName().ToLower())
                    .Enrich.FromLogContext()
                    .Enrich.WithMachineName()
                    .Enrich.WithEnvironmentUserName()
                    .Enrich.WithProcessId()
                    .Enrich.WithProcessName()
                    .Enrich.WithProperty("ThreadId", SystemInfo.CurrentThreadId)
                    .Enrich.WithMemoryUsage()
                    .WriteTo.Seq(SeqHost, apiKey: ApiKey)
                    .CreateLogger()
                )
            {
                foreach (var thisEvent in events)
                {
                    LogEvent(log, thisEvent);
                }
            }
        }

        private static string GetSite()
        {
            return !string.IsNullOrWhiteSpace(Sitecore.Context.Site?.Name) ? Sitecore.Context.Site.Name : null;
        }

        private static string GetRoleName()
        {
            var roleName = Sitecore.Configuration.Settings.GetSetting("CloudRoleNameFallback");

            return !string.IsNullOrWhiteSpace(roleName) ? roleName : "RoleName";
        }

        protected override bool RequiresLayout => true;

        private void LogEvent(ILogger log, LoggingEvent loggingEvent)
        {
            try
            {
                var message = RenderLoggingEvent(loggingEvent);
                var level = GetLogEventLevel(loggingEvent.Level.ToString());
                log.Write(level, message);
            }
            catch (Exception ex)
            {
                ErrorHandler.Error("Error occurred while logging the event.", ex);
            }
        }

        private static LogEventLevel GetLogEventLevel(string level, LogEventLevel defaultValue = LogEventLevel.Debug)
        {
            var logEventLevel = defaultValue;
            switch (level.ToLowerInvariant())
            {
                case "debug":
                    logEventLevel = LogEventLevel.Debug;
                    break;
                case "info":
                    logEventLevel = LogEventLevel.Information;
                    break;
                case "warn":
                    logEventLevel = LogEventLevel.Warning;
                    break;
                case "error":
                    logEventLevel = LogEventLevel.Error;
                    break;
                case "fatal":
                    logEventLevel = LogEventLevel.Fatal;
                    break;
            }

            return logEventLevel;
        }
    }
}
```

###### Issues

This implementation contains the following issues:

- No exception stack trace is logged.
- The method `SendBuffer` is not optimized: each execution creates a new Serilog Logger instance.

##### Add Exception Stack Trace and Optimize Performance

###### Source

To fix the issues mentioned above, we changed the source of the appender:

```
using log4net.spi;
using System;
using System.Reflection;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using ILogger = Serilog.ILogger;
using log4net.helpers;
using Serilog.Exceptions;
using SitecoreSerilog.Extensions;

namespace log4net.Appender
{
    public class SerilogAppender : BufferingAppenderSkeleton
    {
        public string MinimumLevel { get; set; }

        public string ApiKey { get; set; }

        public string SeqHost { get; set; }

        [Obsolete("Use the BufferingAppenderSkeleton Fix methods")]
        public bool LocationInfo => false;

        private Logger _seqLogger;

        private static readonly FieldInfo ExceptionField = typeof(LoggingEvent).GetField("m_thrownException",
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

        protected override void SendBuffer(LoggingEvent[] events)
        {
            if (_seqLogger == null)
            {
                return;
            }

            foreach (var thisEvent in events)
            {
                LogEvent(_seqLogger, thisEvent);
            }
        }

        protected override bool RequiresLayout => true;

        private void LogEvent(ILogger log, LoggingEvent loggingEvent)
        {
            try
            {
                var message = RenderLoggingEvent(loggingEvent);
                var level = GetLogEventLevel(loggingEvent.Level.ToString());
                Exception exception = null;
                if (level >= LogEventLevel.Error)
                {
                    exception = ExceptionField.GetValue(loggingEvent) as Exception;
                }

                log.Write(level, exception, message);
            }
            catch (Exception ex)
            {
                ErrorHandler.Error("Error occurred while logging the event.", ex);
            }
        }

        private static LogEventLevel GetLogEventLevel(string level, LogEventLevel defaultValue = LogEventLevel.Debug)
        {
            var logEventLevel = defaultValue;
            switch (level.ToLowerInvariant())
            {
                case "debug":
                    logEventLevel = LogEventLevel.Debug;
                    break;
                case "info":
                    logEventLevel = LogEventLevel.Information;
                    break;
                case "warn":
                    logEventLevel = LogEventLevel.Warning;
                    break;
                case "error":
                    logEventLevel = LogEventLevel.Error;
                    break;
                case "fatal":
                    logEventLevel = LogEventLevel.Fatal;
                    break;
            }

            return logEventLevel;
        }

        public override void ActivateOptions()
        {
            base.ActivateOptions();
            if (string.IsNullOrEmpty(SeqHost))
            {
                return;
            }
            _seqLogger = new LoggerConfiguration()
                .MinimumLevel
                .ControlledBy(new LoggingLevelSwitch(GetLogEventLevel(MinimumLevel, LogEventLevel.Information)))
                .Enrich.WithFuncEnricher("ApplicationName", GetSite)
                .Enrich.WithFuncEnricher("SpokeName", () => GetRoleName().ToLower())
                .Enrich.FromLogContext()
                .Enrich.WithUtcTimestamp()
                .Enrich.WithMachineName()
                .Enrich.WithEnvironmentUserName()
                .Enrich.WithProcessId()
                .Enrich.WithProcessName()
                .Enrich.WithFuncEnricher("ThreadId", () => SystemInfo.CurrentThreadId)
                .Enrich.WithMemoryUsage()
                .Enrich.WithExceptionDetails()
                .WriteTo.Seq(SeqHost, apiKey: ApiKey, batchPostingLimit: BufferSize)
                .CreateLogger();
            _seqLogger.Information("SEQ Initialized");
        }

        public override void OnClose()
        {
            base.OnClose();
            _seqLogger.Information("SEQ Disposing");
            _seqLogger?.Dispose();
        }

        private static string GetSite()
        {
            return !string.IsNullOrWhiteSpace(Sitecore.Context.Site?.Name) ? Sitecore.Context.Site.Name : "N/A";
        }

        private static string GetRoleName()
        {
            var roleName = Sitecore.Configuration.Settings.GetSetting("CloudRoleNameFallback");

            return !string.IsNullOrWhiteSpace(roleName) ? roleName : "RoleName";
        }
    }
}
```

###### Explanation

**Exception** is added to the logging event using reflection (`m_thrownException` is a private field in `log4net.spi.LoggingEvent`).

```
https://www.brimit.com// use reflection, because m_thrownException is private field in log4net.spi.LoggingEvent
FieldInfo ExceptionField = typeof(LoggingEvent).GetField("m_thrownException",
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
https://www.brimit.com// ...
https://www.brimit.com// call this to ensure exception details are added to logging platform
new LoggerConfiguration()
    // ...
    .Enrich.WithExceptionDetails()
https://www.brimit.com// ...
Exception exception = null;
if (level >= LogEventLevel.Error)
{
    exception = ExceptionField.GetValue(loggingEvent) as Exception;
}
https://www.brimit.com// log exception as well
log.Write(level, exception, message);
```

**Performance** is now better, because only a single Serilog instance is created. To achieve this, we use the `ActivateOptions` and `OnClose` appender lifecycle methods.

##### Context-Dependent Enrichers

The last variant is working, but if you need some context-dependent enrichers (e.g., log information about **Sitecore or HTTP context**), some information will be invalid. This is because `BufferingAppenderSkeleton` is used: enrichers are executed in `SendBuffer` when the buffer is collected, so the context information is different there. To avoid this, `AppenderSkeleton` should be used as the base class, while buffering should be implemented using Serilog Sink configuration (e.g., with SEQ: `.WriteTo.Seq(SeqHost, apiKey: ApiKey, period: TimeSpan.FromSeconds(10));`).

#### Conclusion

All these features are implemented in the [SitecoreSerilog NuGet package](https://www.nuget.org/packages/SitecoreSerilog). Sources for the package are on [Github](https://github.com/izharikov/SitecoreSerilog).

All the source change steps are provided in the Github Gist [here](https://gist.github.com/izharikov/42801610f971b8d7981d35f04f45083d).

###### Author

[!\[Igor Zharikov - Senior Sitecore Developer, Sitecore MVP\](https://www.brimit.com/-/jssmedia/feature/blogs/authors/igor-zharikov-2.jpg?h=700&amp;iar=0&amp;w=700&amp;hash=292A84E8CB3E5CE57CA743385CFB1464)
Igor Zharikov
Sitecore MVP / Senior Sitecore Developer](https://www.brimit.com/blog/author?authors=Igor%20Zharikov)

#### More on Sitecore

[!\[How Vercel Will Help You Save Effort When Deploying Sophisticated Sitecore Projects\](https://www.brimit.com/-/jssmedia/project/brimit/blog/2024/vercel_cover-image.png)
#Guides#How-toDXPE-commerce
##### How Vercel Will Help You Save Effort When Deploying Sophisticated Sitecore Projects
Optimize and accelerate the development and deployment of complex multisite Sitecore projects.
Alexei Vershalovich on July 17, 2024](https://www.brimit.com/blog/how-vercel-will-help-you-save-effort-when-deploying-sophisticated-sitecore-projects)

[!\[Training Up Tomorrow's Sitecore MVPs: a Mentoring Success Story\](https://www.brimit.com/-/jssmedia/project/brimit/blog/2023/sitecore-mentoring---cover-image.png)
#How-toDXP
##### Training Up Tomorrow's Sitecore MVPs: a Mentoring Success Story
How to participate in the Sitecore Mentor program and help younger colleagues jump-start a career in Sitecore development.
Sergey Baranov on October 2, 2023](https://www.brimit.com/blog/training-up-tomorrows-sitecore-mvps)

[!\[Going Headless. Part 2: When a Headless CMS Is Your Best Bet (if you have Sitecore)\](https://www.brimit.com/-/jssmedia/project/brimit/blog/2022/headless/adobestock_456986731.jpg)
#How-toDXPE-commerce
##### Going Headless. Part 2: When a Headless CMS Is Your Best Bet (if you have Sitecore)
Discover how a headless CMS can benefit organizations that use Sitecore.
Daniil Raschupkin, Palina Trokhautsava on September 15, 2022](https://www.brimit.com/blog/going-headless-part-2-when-a-headless-cms-is-your-best-bet-if-you-have-sitecore)

![](https://bat.bing.net/action/0?ti=187017043&amp;tm=gtm002&amp;Ver=2&amp;mid=c0b20992-04b3-4258-a15e-b599be7b2d47&amp;bo=2&amp;gtm_tag_source=1&amp;pi=0&amp;lg=en-US&amp;sw=800&amp;sh=600&amp;sc=24&amp;nwd=1&amp;tl=Sitecore%20Serilog%20Appender&amp;kw=sitecore,logging,serilog,seq&amp;p=https%3A%2F%2Fwww.brimit.com%2Fblog%2Fsitecore-serilog-logging&amp;r=&amp;lt=290&amp;evt=pageLoad&amp;sv=2&amp;asc=D&amp;cdb=AQAY&amp;rn=396084)