Application Logging in Episerver DXC

In standard DXC Service setup, application logs are stored in BLOB storage with 90 days retention. They can be inspected in the PaaS portal using custom Log Stream or downloaded in CSV format for offline analysis. DXC Service comes with an Application Insights account, which can be used as a nice alternative to standard application logging.

What's the problem with standard logging?

I experienced two big issues:

  • The log level is set to Error. This can be temporarily changed by creating a support ticket, but I wanted a permanent solution, not a temporary one. Being able to see information level messages in Log Stream is very useful when debugging integrations with external systems
  • Reading through many CSV files is very time-consuming, not human-friendly, etc.

Application Insights Comes to the Rescue!

One way to approach this would be to use EPiServer.Logging.LogManager abstraction and EPiServer.Logging.TraceLoggerFactory for Episerver stuff, and the Application Insights API to write custom log messages. This could work; however, I prefer to have both Episerver log messages and custom application messages saved in one place.

Another option would be to use Microsoft.ApplicationInsights.EventSourceListener, which listens to System.Diagnostics.Tracing.EventSource events and logs them to Application Insights. The problem with this approach is that error log messages are written as Traces, which makes it difficult to have an overview of critical error messages.

The third option, which I like the most, is to write a custom log factory that writes to both .NET Diagnostics Trace (recommended/required by Episerver), and Application Insights at the same time.

Implementation

The first thing we need to do is to install Microsoft.ApplicationInsights NuGet package.

Next, we need to set the instruction key by adding the following line inside Global.asax.cs/Application_Start method:

Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.Active.InstrumentationKey = ConfigurationManager.AppSettings["APPINSIGHTS_INSTRUMENTATIONKEY"];

Then we need to create a custom log factory:

public class MyLoggerFactory : ILoggerFactory
{
    public ILogger Create(string name)
    {
        return new MyLogger(name);
    }
}

public class MyLogger : ILogger
{
    private readonly string _name;
    private readonly TelemetryClient _telemetryClient;

    public MyLogger(string name)
    {
        _name = name;
        _telemetryClient = new TelemetryClient();
    }

    public bool IsEnabled(Level level)
    {
        if ((int)level < (int)Level.Information)
        {
            return false;
        }

        return Trace.Listeners.Count > 0;
    }

    public void Log<TState, TException>(
        Level level,
        TState state,
        TException exception,
        Func<TState, TException, string> messageFormatter,
        Type boundaryType) where TException : Exception
    {
        if (!IsEnabled(level) || messageFormatter == null)
        {
            return;
        }

        string msg = _name + " : " + messageFormatter(state, exception);

        if (level == Level.Information)
        {
            LogInformation(msg);
        }

        if (level == Level.Warning)
        {
            LogWarning(msg);
        }

        if (level == Level.Error ||
            level == Level.Critical)
        {
            LogError(state, msg, exception);
        }
    }

    private void LogInformation(string message)
    {
        Trace.TraceInformation(message);

        var trace = new TraceTelemetry(message) { SeverityLevel = SeverityLevel.Information };
        _telemetryClient.Track(trace);
    }

    private void LogWarning(string message)
    {
        Trace.TraceWarning(message);

        var trace = new TraceTelemetry(message) { SeverityLevel = SeverityLevel.Warning };
        _telemetryClient.Track(trace);
    }

    private void LogError<TState, TException>(
        TState state,
        string traceMessage,
        TException exception)
        where TException : Exception
    {
        Trace.TraceError(traceMessage);

        var exceptionTelemetry = new ExceptionTelemetry
        {
            Exception = exception,
            Message = FormatErrorMessage(state, exception)
        };

        _telemetryClient.TrackException(exceptionTelemetry);
    }

    private string FormatErrorMessage<TState, TException>(
        TState state,
        TException exception)
        where TException : Exception
    {
        string stateMessage = state as string;
        string errorMessage = exception?.Message;

        if (!string.IsNullOrWhiteSpace(stateMessage) &&
            !string.IsNullOrWhiteSpace(errorMessage))
        {
            return stateMessage + ": " + errorMessage;
        }

        if (!string.IsNullOrWhiteSpace(stateMessage))
        {
            return stateMessage;
        }

        if (!string.IsNullOrWhiteSpace(errorMessage))
        {
            return errorMessage;
        }

        return null;
    }
}

 And register it in web.config by adding the following app setting:

<add key="episerver:LoggerFactoryType" value="MyNamespace.MyLoggerFactory, MyAssemblyName" />

Remember to replace MyNamespace and MyAssemblyName with real values :)

Application Settings

All DXC Service environments come with pre-configured application settings, which override the application settings in web.config. Some of those settings are APPINSIGHTS_INSTRUMENTATIONKEY and episerver:LoggerFactoryType. episerver:LoggerFactory is set to EPiServer.Logging.TraceLoggerFactory by default, so to use your custom logger factory, you have to create a support ticket and ask Episerver to remove that setting from all DXC environments.

Troubleshooting

You should be able to see log messages in both Log Stream (PaaS portal) and Application Insights (Azure Portal). It takes some time (a few minutes, usually) before log messages are available in Application Insights.

If you don’t see log messages in neither Log Stream or Application Insights, make sure that Tracing is enabled and that you're using correct logger factory.

Tracing is not enabled by default. You have to enable Tracing when you build the application, or by adding #define TRACE on top of MyLogger.cs file (first line, before using statements).

The best way to check if you're using correct logger factory is to write a tool inside a password protected area of your site, iterate through ConfigurationManager.AppSettings and print out all key-value pairs.

Happy logging!

comments powered by Disqus