DEV Community

Cover image for Log Aggregation in .NET 8: Seq vs ELK vs Loki
Anaya Upadhyay
Anaya Upadhyay

Posted on • Originally published at instagram.com

Log Aggregation in .NET 8: Seq vs ELK vs Loki

I have seen the same setup at more companies than I can count: every service writing logs to stdout, a few rotating files scattered across VMs, maybe one service sending to Application Insights, and nobody quite sure where the logs from the background jobs go.

It works fine until something breaks in production. Then you have an error, four services, and zero way to trace a single request across any of them.

That is the problem log aggregation solves. This article covers what it actually means in a .NET 8 stack, how to set it up with Serilog, and how to pick the right aggregator for where your team is right now.


What log aggregation actually means

The idea is simple: every service in your system writes structured log events to a central store, and you query that store instead of grepping files.

The pipeline has three parts:

  1. Your app - calls ILogger<T> with structured message templates
  2. A sink - Serilog serializes and ships the event to a destination
  3. An aggregator - receives, indexes, and stores the events so you can query them by any property

The key word is structured. If your log messages are plain strings, you have traded one grep problem for another. The entire value of aggregation comes from being able to filter on OrderId, CorrelationId, CustomerId, or any other property you attached to the event.


Setting up Serilog with multiple sinks

Sinks are composable. You can write to Seq locally, Elasticsearch in production, and a rolling file as a fallback, all from the same configuration.

// Program.cs
Log.Logger = new LoggerConfiguration()
    .Enrich.WithCorrelationId()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
    .MinimumLevel.Override("System", LogEventLevel.Warning)
    .WriteTo.Seq("http://localhost:5341")
    .WriteTo.Elasticsearch(opts =>
    {
        opts.IndexFormat = "logs-{0:yyyy.MM}";
        opts.AutoRegisterTemplate = true;
    })
    .WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day)
    .CreateLogger();

builder.Host.UseSerilog();
Enter fullscreen mode Exit fullscreen mode

A few things worth noting here. The MinimumLevel.Override calls are important in production. Without them, Microsoft framework internals log at Debug and they will flood your aggregator with noise that buries your actual signals. Set them to Warning in production environments.

The Enrich.WithCorrelationId() call requires the Serilog.Enrichers.CorrelationId package. Pair it with a middleware that sets BeginScope on every request and every log line inside that request will carry the same correlation ID. Without it, log aggregation gives you a better filing cabinet but not a better answer.


Seq: start here

If your team is not running a local aggregator today, start with Seq. It is free for individual use, has first-class support for structured .NET logs, and takes about two minutes to set up.

docker run --rm -e ACCEPT_EULA=Y -p 5341:5341 -p 80:80 datalust/seq:latest
Enter fullscreen mode Exit fullscreen mode

Then in appsettings.Development.json:

"Serilog": {
  "WriteTo": [{
    "Name": "Seq",
    "Args": { "serverUrl": "http://localhost:5341" }
  }]
}
Enter fullscreen mode Exit fullscreen mode

Open http://localhost and you will see every structured event your application emits. Filter by @Level = 'Error', or OrderId = '123', or CorrelationId = 'abc' - all of it works immediately because the properties are indexed, not buried inside a string.

The query experience alone is worth it for local development. You stop reading raw console output and start asking questions.


Picking a production aggregator

Here is a direct comparison of the three options most .NET teams evaluate.

Seq

Good for teams up to maybe 20 engineers with moderate log volume. The single-node architecture is a real ceiling but it is not a problem until it is a problem. Reasonably priced for small teams and the operational overhead is low. If you are a startup or a small product team, Seq in production is a completely legitimate choice.

ELK Stack (Elasticsearch, Logstash, Kibana)

The right choice when you have high volume, need full-text search across log content, or have an ops team that can own the infrastructure. Kibana dashboards are genuinely good for sharing observability across engineering and operations. The tradeoff is real though: you are running three services, Elasticsearch is resource-hungry, and the licensing situation has changed a few times in recent years - worth reviewing before you commit. Not a good first production choice for a small team.

Grafana Loki

Loki takes a different approach. Instead of indexing the full content of every log line, it indexes labels only. This makes it significantly cheaper at scale. The tradeoff is that you cannot do full-text search across log content by default - you query by labels and then filter within results.

If your team is already running Grafana and Prometheus, Loki is the natural addition. It integrates tightly with both and keeps your observability stack in one place. On Kubernetes it scales well horizontally. The LogQL query language takes getting used to, but it is not complex.


The three mistakes that break aggregation in production

String concatenation in message templates. The moment you write _logger.LogInformation("Order " + id + " placed"), the structured property is gone. The aggregator receives a plain string. You cannot filter on OrderId in Seq or Kibana because it does not exist as a property. Always use message templates: _logger.LogInformation("Order {OrderId} placed", id).

Console.WriteLine anywhere in your codebase. It bypasses every sink. It is unstructured, and in a containerised environment it is lost on restart. Any Console.WriteLine in application code should be replaced with ILogger. This includes library code you own.

No minimum level override per environment. Framework-level components log a lot at Debug. In production, shipping those events to your aggregator wastes storage and makes real errors harder to find. A MinimumLevel.Override for Microsoft.* and System.* set to Warning is one configuration line that keeps your signal-to-noise ratio sane.


Before you configure anything

The aggregator is the last decision, not the first.

Get structured message templates right across your codebase. Set up a correlation ID scope in middleware so every log line in a request shares an ID. Pick the minimum level that makes sense for each environment.

Once those are in place, the aggregator almost does not matter. Seq, ELK, Loki - they are all just different query UIs sitting on top of the structured events your app already emits correctly.


This article is part of Logging in .NET - Series 2. The full carousel version with sink configuration code and a setup checklist is on Instagram at @thesharpfuture.

Top comments (0)