Serilog with noise filtering and a simple daily report

Most apps already log something. The problem is that the useful events are buried under framework chatter, duplicated request lines, and low-value noise. The goal is not more logs. The goal is logs you can trust during an incident, and logs you can use later for reporting.

This article shows a clean Serilog setup in .NET 10, plus two ways to generate a daily transaction report.

What good looks like

A log event should answer:

If you nail those, debugging gets faster and reporting becomes cheap.

Step 1: Add packages

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Settings.Configuration
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File

Optional, if you want SQL storage:

dotnet add package Serilog.Sinks.MSSqlServer

Step 2: Configure Serilog in Program.cs (noise-aware)

This baseline enriches events with request context, filters common low value paths, turns down Microsoft logs while keeping warnings and errors, and writes to console plus a daily rolling file.

using Serilog;
using Serilog.Context;

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseSerilog((context, services, loggerConfig) =>
{
    loggerConfig
        .ReadFrom.Configuration(context.Configuration)
        .ReadFrom.Services(services)
        .Enrich.FromLogContext()
        .Enrich.WithProperty("Application", builder.Environment.ApplicationName)
        .Enrich.WithProperty("Environment", builder.Environment.EnvironmentName)

        .MinimumLevel.Information()
        .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning)
        .MinimumLevel.Override("Microsoft.AspNetCore", Serilog.Events.LogEventLevel.Warning)
        .MinimumLevel.Override("System", Serilog.Events.LogEventLevel.Warning)

        .Filter.ByExcluding(le =>
        {
            if (!le.Properties.TryGetValue("RequestPath", out var pathValue)) return false;
            var path = pathValue.ToString().Trim('"');

            return path.StartsWith("/health", StringComparison.OrdinalIgnoreCase)
                   || path.Equals("/favicon.ico", StringComparison.OrdinalIgnoreCase);
        })

        .WriteTo.Console()
        .WriteTo.File(
            path: "Logs/log-.ndjson",
            rollingInterval: RollingInterval.Day,
            retainedFileCountLimit: 30,
            shared: true
        );
});

builder.Services.AddHttpContextAccessor();
builder.Services.AddControllers();

var app = builder.Build();

app.Use(async (ctx, next) =>
{
    var incoming = ctx.Request.Headers.TryGetValue("X-Correlation-Id", out var cid)
        ? cid.ToString()
        : ctx.TraceIdentifier;

    using (LogContext.PushProperty("CorrelationId", incoming))
    using (LogContext.PushProperty("RequestPath", ctx.Request.Path.Value ?? ""))
    using (LogContext.PushProperty("RequestMethod", ctx.Request.Method))
    {
        await next();
    }
});

app.UseSerilogRequestLogging(opts =>
{
    opts.EnrichDiagnosticContext = (diagCtx, httpCtx) =>
    {
        diagCtx.Set("ClientIp", httpCtx.Connection.RemoteIpAddress?.ToString());
        diagCtx.Set("UserAgent", httpCtx.Request.Headers.UserAgent.ToString());
    };
});

app.MapControllers();
app.Run();

Step 3: Keep appsettings.json readable

{
  "Serilog": {
    "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "Microsoft.AspNetCore": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      { "Name": "Console" },
      {
        "Name": "File",
        "Args": {
          "path": "Logs/log-.ndjson",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 30,
          "shared": true
        }
      }
    ],
    "Enrich": [ "FromLogContext" ],
    "Properties": {
      "Service": "MyService"
    }
  }
}

Step 4: Log events you can count later

Avoid vague logs like this:

logger.LogInformation("User searched for something");

Prefer consistent events with stable properties:

using Microsoft.AspNetCore.Mvc;
using Serilog;

[ApiController]
[Route("api/search")]
public class SearchController : ControllerBase
{
    [HttpGet]
    public IActionResult Get([FromQuery] string postcode)
    {
        var normalised = postcode?.Trim().ToUpperInvariant();

        Log.ForContext("EventName", "AddressSearch")
           .ForContext("Postcode", normalised)
           .Information("Address search requested");

        // Do work...

        Log.ForContext("EventName", "AddressSearchCompleted")
           .ForContext("Postcode", normalised)
           .ForContext("ResultCount", 12)
           .Information("Address search completed");

        return Ok(new { results = 12 });
    }
}

With that, reporting can count EventName = AddressSearchCompleted without parsing message text.

Step 5: Two approaches for a daily report

Option A: Report from SQL (good for dashboards)

If you want a report page inside your app, SQL tends to be the easiest.

Add an MSSQL sink:

// Requires Serilog.Sinks.MSSqlServer
// loggerConfig.WriteTo.MSSqlServer(
//     connectionString: context.Configuration.GetConnectionString("LogsDb"),
//     sinkOptions: new Serilog.Sinks.MSSqlServer.Sinks.MSSqlServerSinkOptions
//     {
//         TableName = "log_events",
//         AutoCreateSqlTable = true
//     });

Then query daily counts:

SELECT
    CAST([TimeStamp] AS date) AS [Day],
    COUNT(*) AS [TotalCompletedSearches]
FROM dbo.log_events
WHERE Properties LIKE '%"EventName":"AddressSearchCompleted"%'
GROUP BY CAST([TimeStamp] AS date)
ORDER BY [Day] DESC;

This works as a quick start. If you want it cleaner long term, store EventName as a dedicated column using additional column options, and query that column instead of searching inside Properties.

Option B: Report from rolling files (fast, no database)

If you write daily NDJSON files, you can read yesterday’s file and count events.

using System.Text.Json;

public static class DailyLogReport
{
    public static int CountEvents(string filePath, string eventName)
    {
        if (!File.Exists(filePath)) return 0;

        var count = 0;

        foreach (var line in File.ReadLines(filePath))
        {
            if (string.IsNullOrWhiteSpace(line)) continue;

            using var doc = JsonDocument.Parse(line);

            if (!doc.RootElement.TryGetProperty("Properties", out var props)) continue;
            if (!props.TryGetProperty("EventName", out var ev)) continue;

            if (ev.GetString() == eventName) count++;
        }

        return count;
    }
}

Usage:

var date = DateTime.UtcNow.Date.AddDays(-1);
var path = $"Logs/log-{date:yyyyMMdd}.ndjson";

var total = DailyLogReport.CountEvents(path, "AddressSearchCompleted");
Console.WriteLine($"Completed searches for {date:yyyy-MM-dd}: {total}");

Filtering tips that pay off

A checklist you can copy into your README

Wrap up

Once you have structured logs, filtering, and an event naming convention, you stop reading logs and start using logs. Incidents get easier to diagnose, and basic reporting becomes a byproduct of normal engineering.