Log without Exception stack trace in Serilog

6.4k Views Asked by At

I am using Serilog and Serilog.Exceptions for structured logging purposes in my MVC5 application.

I want to remove the stack trace from the value of the "Exception" field in the log. I understand that I am using the new JsonFormatter() in the RollingFile sink, hence the exception object, say, ex, gets formatted as ex.ToString() and the entire object gets written as it is.

I am using new DestructuringOptionsBuilder().WithDefaultDestructurers() to show stack trace in "ExceptionDetails" section which looks much better as a separate field.

Is it possible to see only the name of the exception as a value of the "Exception" field in the log rather than the entire stack trace and other details as ex.ToString() while writing to a JSON file sink?

Here is my exception log:

{
  "Timestamp": "2021-09-02T15:04:02.4469999+05:00",
  "Level": "Error",
  "MessageTemplate": "Unhandled Exception",
  "Exception": "System.NullReferenceException: Object reference not set to an instance of an object.\r\n   at AppV2.Controllers.BaseController.Initialize(RequestContext requestContext) in E:\\Workspace\\AppV2\\AppV2\\Controllers\\BaseController.cs:line 115\r\n   at System.Web.Mvc.Controller.BeginExecute(RequestContext requestContext, AsyncCallback callback, Object state)\r\n   at System.Web.Mvc.Controller.System.Web.Mvc.Async.IAsyncController.BeginExecute(RequestContext requestContext, AsyncCallback callback, Object state)\r\n   at System.Web.Mvc.MvcHandler.<BeginProcessRequest>b__4(AsyncCallback asyncCallback, Object asyncState, ProcessRequestState innerState)\r\n   at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncVoid`1.CallBeginDelegate(AsyncCallback callback, Object callbackState)\r\n   at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResultBase`1.Begin(AsyncCallback callback, Object state, Int32 timeout)\r\n   at System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, Object state)\r\n   at System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, Object state)\r\n   at System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData)\r\n   at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()\r\n   at System.Web.HttpApplication.ExecuteStepImpl(IExecutionStep step)\r\n   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)",
  "Properties": {
    "UserName": "[email protected]",
    "ThreadId": 5,
    "Caller": "AppV2.Extensions.Logger.LogError(System.Exception, System.String, System.Object[])",
    "MachineName": "DESKTOP-GHV3V41",
    "HttpRequestId": "e9922caf-7e25-47f8-9941-263ba1ec4278",
    "HttpRequestNumber": 1,
    "HttpRequestClientHostIP": "::1",
    "HttpRequestType": "GET",
    "HttpRequestRawUrl": "/",
    "ExceptionDetails": {
      "Type": "System.NullReferenceException",
      "HResult": -2147467261,
      "Message": "Object reference not set to an instance of an object.",
      "Source": "Boilerplate.Web.Mvc5.Sample",
      "StackTrace": "   at AppV2.Controllers.BaseController.Initialize(RequestContext requestContext) in E:\\Workspace\\AppV2\\AppV2\\Controllers\\BaseController.cs:line 115\r\n   at System.Web.Mvc.Controller.BeginExecute(RequestContext requestContext, AsyncCallback callback, Object state)\r\n   at System.Web.Mvc.Controller.System.Web.Mvc.Async.IAsyncController.BeginExecute(RequestContext requestContext, AsyncCallback callback, Object state)\r\n   at System.Web.Mvc.MvcHandler.<BeginProcessRequest>b__4(AsyncCallback asyncCallback, Object asyncState, ProcessRequestState innerState)\r\n   at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncVoid`1.CallBeginDelegate(AsyncCallback callback, Object callbackState)\r\n   at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResultBase`1.Begin(AsyncCallback callback, Object state, Int32 timeout)\r\n   at System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, Object state)\r\n   at System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, Object state)\r\n   at System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData)\r\n   at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()\r\n   at System.Web.HttpApplication.ExecuteStepImpl(IExecutionStep step)\r\n   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)",
      "TargetSite": "Void Initialize(System.Web.Routing.RequestContext)"
    }
  }
}

Here is my Logger class:

public class Logger
{
    private static readonly ILogger logger;

    static Logger()
    {
        logger = new LoggerConfiguration()
            .Enrich.WithUserName(anonymousUsername: "Not Authenticated")
            .Enrich.FromLogContext()
            .Enrich.With(new ThreadEnrich())
            .Enrich.WithCaller()
            .Enrich.WithMachineName()
            .Enrich.WithHttpRequestId()
            .Enrich.WithHttpRequestNumber()
            .Enrich.WithHttpRequestClientHostIP()
            .Enrich.WithHttpRequestType()
            .Enrich.WithHttpRequestRawUrl()
            .Enrich.WithMvcRouteData()
            .Enrich.WithExceptionDetails(
                new DestructuringOptionsBuilder()
                .WithDefaultDestructurers())
            .Enrich.WithDemystifiedStackTraces()
            .WriteTo.RollingFile(new JsonFormatter(),
                HttpContext.Current.Server.MapPath($"~/logs/log-.json"),
                LogEventLevel.Debug,
                fileSizeLimitBytes: 655360)
            .CreateLogger();
    }

    public static void LogInformation(string info, object[] data = null)
    {
        logger.Information(info, data);
    }

    public static void LogDebug(string debug, object[] data = null)
    {
        logger.Debug(debug, data);
    }

    public static void LogWarning(string warning, object[] data = null, Exception e = null)
    {
        logger.Warning(e, warning, data);
    }

    public static void LogError(Exception e, string error, object[] data = null)
    {
        logger.Error(e, error, data);
    }
}

Any suggestions regarding my Logger class are also welcome.

2

There are 2 best solutions below

1
On

I researched and I think you should check the Serilog filters. You can filter event fields, sources or even filter data if they have a certain value, for example. You can do this by configuring the respective filters in your logger configuration on code or in the JSON file.

Refer to this link for more information https://github.com/serilog/serilog-filters-expressions

2
On

Regardless of what log properties exist, "formatters" are how Serilog transforms standard log information (including exceptions) into text.

To get rid of the redundancy, you'll need to use a something other than JsonFormatter.

If you use Serilog.Expressions, this is easily done with an ExpressionTemplate. Example usage with Serilog.Sinks.File (other sinks are similar):

const string logTemplate = "{ { Timestamp: @t, Message: @m, Level: @l, Exception: ExceptionDetail, ..Rest() } }\n";

Log.Logger = new LoggerConfiguration()
  .Enrich.WithExceptionDetails()
  .WriteTo.File(new ExpressionTemplate(logTemplate), "app.log")
  .CreateLogger();

OLD ANSWER (without Serilog.Expressions):

Here's a formatter that I use that simply moves "ExceptionDetails" in place of the top-level "Exception."

namespace Serilog.Exceptions.Formatting;

using System.IO;
using Serilog.Events;
using Serilog.Exceptions.Core;
using Serilog.Formatting;
using Serilog.Formatting.Json;

/// <summary>
/// A JSON text formatter using structured properties for exceptions.
/// </summary>
/// <remarks>
/// Avoids the redundancy of <see cref="JsonFormatter"/> when used with <see cref="ExceptionEnricher"/>.
/// </remarks>
public class StructuredExceptionFormatter : ITextFormatter
{
    private readonly string rootName;
    private readonly JsonValueFormatter valueFormatter = new(typeTagName: null);

    /// <summary>
    /// Initializes a new instance of the <see cref="StructuredExceptionFormatter"/> class.
    /// </summary>
    /// <param name="rootName">The root name used by the enricher, if different from the default.</param>
    public StructuredExceptionFormatter(string? rootName = null)
    {
        this.rootName = rootName ?? new DestructuringOptionsBuilder().RootName;
    }

    public void Format(LogEvent logEvent, TextWriter output)
    {
        output.Write("{\"Timestamp\":\"");
        output.Write(logEvent.Timestamp.UtcDateTime.ToString("O"));

        output.Write("\",\"Message\":");
        var message = logEvent.MessageTemplate.Render(logEvent.Properties);
        JsonValueFormatter.WriteQuotedJsonString(message, output);

        output.Write(",\"Level\":\"");
        output.Write(logEvent.Level);
        output.Write('\"');

        var propCount = logEvent.Properties.Count;

        if (logEvent.Properties.TryGetValue(this.rootName, out var exceptionProperty))
        {
            output.Write(",\"Exception\":");
            this.valueFormatter.Format(exceptionProperty, output);
            propCount--;
        }

        if (propCount > 0)
        {
            output.Write(",\"Properties\":{");
            var comma = false;

            foreach (var property in logEvent.Properties)
            {
                if (property.Key == this.rootName)
                {
                    continue;
                }

                if (comma)
                {
                    output.Write(',');
                }
                else
                {
                    comma = true;
                }

                JsonValueFormatter.WriteQuotedJsonString(property.Key, output);
                output.Write(':');
                this.valueFormatter.Format(property.Value, output);
            }

            output.Write("}");
        }

        output.WriteLine('}');
    }
}

It would render your exception as follows:

{
  "Timestamp": "2021-09-02T15:04:02.4469999+05:00",
  "Level": "Error",
  "Message": "Unhandled Exception",
  "Exception": {
    "Type": "System.NullReferenceException",
    "HResult": -2147467261,
    "Message": "Object reference not set to an instance of an object.",
    "Source": "Boilerplate.Web.Mvc5.Sample",
    "StackTrace": "   at AppV2.Controllers.BaseController.Initialize(RequestContext requestContext) in E:\\Workspace\\AppV2\\AppV2\\Controllers\\BaseController.cs:line 115\r\n   at System.Web.Mvc.Controller.BeginExecute(RequestContext requestContext, AsyncCallback callback, Object state)\r\n   at System.Web.Mvc.Controller.System.Web.Mvc.Async.IAsyncController.BeginExecute(RequestContext requestContext, AsyncCallback callback, Object state)\r\n   at System.Web.Mvc.MvcHandler.<BeginProcessRequest>b__4(AsyncCallback asyncCallback, Object asyncState, ProcessRequestState innerState)\r\n   at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncVoid`1.CallBeginDelegate(AsyncCallback callback, Object callbackState)\r\n   at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResultBase`1.Begin(AsyncCallback callback, Object state, Int32 timeout)\r\n   at System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, Object state)\r\n   at System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, Object state)\r\n   at System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData)\r\n   at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()\r\n   at System.Web.HttpApplication.ExecuteStepImpl(IExecutionStep step)\r\n   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)",
    "TargetSite": "Void Initialize(System.Web.Routing.RequestContext)"
  },
  "Properties": {
    "UserName": "[email protected]",
    "ThreadId": 5,
    "Caller": "AppV2.Extensions.Logger.LogError(System.Exception, System.String, System.Object[])",
    "MachineName": "DESKTOP-GHV3V41",
    "HttpRequestId": "e9922caf-7e25-47f8-9941-263ba1ec4278",
    "HttpRequestNumber": 1,
    "HttpRequestClientHostIP": "::1",
    "HttpRequestType": "GET",
    "HttpRequestRawUrl": "/"
  }
}