<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anaya Upadhyay</title>
    <description>The latest articles on DEV Community by Anaya Upadhyay (@anayaupadhyay).</description>
    <link>https://dev.to/anayaupadhyay</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1736805%2F1b89cbeb-ce35-4c87-a7a5-a14dfb431e88.png</url>
      <title>DEV Community: Anaya Upadhyay</title>
      <link>https://dev.to/anayaupadhyay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anayaupadhyay"/>
    <language>en</language>
    <item>
      <title>Log Aggregation in .NET 8: Seq vs ELK vs Loki</title>
      <dc:creator>Anaya Upadhyay</dc:creator>
      <pubDate>Tue, 26 May 2026 09:15:00 +0000</pubDate>
      <link>https://dev.to/anayaupadhyay/log-aggregation-in-net-8-seq-vs-elk-vs-loki-437l</link>
      <guid>https://dev.to/anayaupadhyay/log-aggregation-in-net-8-seq-vs-elk-vs-loki-437l</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What log aggregation actually means
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The pipeline has three parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your app&lt;/strong&gt; - calls &lt;code&gt;ILogger&amp;lt;T&amp;gt;&lt;/code&gt; with structured message templates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A sink&lt;/strong&gt; - Serilog serializes and ships the event to a destination&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An aggregator&lt;/strong&gt; - receives, indexes, and stores the events so you can query them by any property&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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 &lt;code&gt;OrderId&lt;/code&gt;, &lt;code&gt;CorrelationId&lt;/code&gt;, &lt;code&gt;CustomerId&lt;/code&gt;, or any other property you attached to the event.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting up Serilog with multiple sinks
&lt;/h2&gt;

&lt;p&gt;Sinks are composable. You can write to Seq locally, Elasticsearch in production, and a rolling file as a fallback, all from the same configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Program.cs&lt;/span&gt;
&lt;span class="n"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LoggerConfiguration&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enrich&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithCorrelationId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinimumLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Override&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Microsoft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogEventLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinimumLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Override&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"System"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogEventLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteTo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:5341"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteTo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Elasticsearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IndexFormat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"logs-{0:yyyy.MM}"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AutoRegisterTemplate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteTo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"logs/app-.log"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rollingInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RollingInterval&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateLogger&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSerilog&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The &lt;code&gt;Enrich.WithCorrelationId()&lt;/code&gt; call requires the &lt;code&gt;Serilog.Enrichers.CorrelationId&lt;/code&gt; package. Pair it with a middleware that sets &lt;code&gt;BeginScope&lt;/code&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Seq: start here
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ACCEPT_EULA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Y &lt;span class="nt"&gt;-p&lt;/span&gt; 5341:5341 &lt;span class="nt"&gt;-p&lt;/span&gt; 80:80 datalust/seq:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in &lt;code&gt;appsettings.Development.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"Serilog"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"WriteTo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Seq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"serverUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:5341"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The query experience alone is worth it for local development. You stop reading raw console output and start asking questions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Picking a production aggregator
&lt;/h2&gt;

&lt;p&gt;Here is a direct comparison of the three options most .NET teams evaluate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Seq
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  ELK Stack (Elasticsearch, Logstash, Kibana)
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grafana Loki
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three mistakes that break aggregation in production
&lt;/h2&gt;

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

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

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




&lt;h2&gt;
  
  
  Before you configure anything
&lt;/h2&gt;

&lt;p&gt;The aggregator is the last decision, not the first.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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 &lt;a href="https://www.instagram.com/thesharpfuture" rel="noopener noreferrer"&gt;@thesharpfuture&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>serilog</category>
      <category>aspdotnet</category>
    </item>
  </channel>
</rss>
