Vibe monitoring with Last9 MCP: Ask your agent to fix production issues! Setup →
Last9 Last9

.NET Logging with Serilog and OpenTelemetry

Bring structure and trace context to your .NET logs by combining Serilog with OpenTelemetry for better debugging and observability.

May 21st, ‘25
.NET Logging with Serilog and OpenTelemetry
See How Last9 Works

Unified observability for all your telemetry.Open standards. Simple pricing.

Talk to us

If you're building .NET applications, there's a good chance you're already using Serilog for structured logging. And if you're working with distributed systems or microservices, OpenTelemetry is probably on your radar for tracing and metrics.

Together, Serilog and OpenTelemetry make it easier to connect the dots linking logs to traces so you can understand how your app behaves across services, not just within a single component.

Why Use Serilog with OpenTelemetry?

Serilog makes it simple to create structured logs in .NET, but the real power comes when you connect those logs with OpenTelemetry’s traces and metrics. Using Serilog and OpenTelemetry in .NET 8, you can enrich your logs with trace and span IDs thanks to Serilog’s enrichers that pull in this context automatically.

This means every log entry becomes part of a larger story, helping you follow a user request across services without losing track. Plus, with support for sending logs through the OpenTelemetry Collector, you can centralize your telemetry data in one place, making it easier to monitor, query, and analyze.

How Serilog Made .NET Logging Better

Logging used to be just dumping plain text messages. It worked—until you needed to search logs or filter them. That’s when Serilog came along.

Instead of writing logs like this:

_logger.Information("User 123 purchased 5 items for $200");

You write:

_logger.Information("User {UserId} purchased {ItemCount} items for {TotalAmount}", 
    userId, itemCount, totalAmount);

What’s different?

  • The message is still easy to read.
  • But Serilog also saves UserId, ItemCount, and TotalAmount as separate fields.
  • This means your logs are structured, not just a string.

Why’s that useful? Because structured logs let you:

  • Search for purchases over a certain amount.
  • Filter by user ID.
  • Build alerts or dashboards based on actual data.

And you get all this without making your logging code more complex.

// This sets up Serilog to add OpenTelemetry trace info to your logs

If you use the Serilog OpenTelemetry sink, make sure to configure Activity enrichment. This adds trace context to your logs, so you can link them back to traces in tools like Last9, Honeycomb, or Jaeger.

💡
If you're already using Serilog for structured logs, you might also want to see how Loki handles log management at scale.

OpenTelemetry: One Framework for All Your Observability Data

Apps generate a lot of signals—logs, metrics, and traces. Each gives a different view of what’s happening. The problem is that these signals often end up in different tools, with different formats, making things complicated.

OpenTelemetry fixes this by giving you one framework to collect and handle all your telemetry data.

  • Logs capture events, errors, and messages—the breadcrumbs your app leaves behind.
  • Metrics are numbers that track system health over time, like request rates or memory use.
  • Traces follow a request as it moves through your services, helping you find slowdowns or failures.

OpenTelemetry standardizes how you collect and export this data, so you don’t have to set up separate tools for each signal. If you’re working with .NET, it fits right in and helps keep your observability consistent.

It’s supported by major cloud providers and observability platforms like Last9, so it’s a safe and practical choice for modern distributed systems.

How to Connect Serilog with OpenTelemetry

If you want your logs, traces, and metrics to play nice together, this setup gets you there without a headache.

Step 1: Get the OpenTelemetry Collector Running

The Collector is like a mailman. Your app sends telemetry (logs, traces, metrics) to the Collector. Then the Collector delivers it to your observability tool (like Last9).

Here’s a quick way to start it with Docker:

services:
  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    volumes:
      - ./otel-collector-config.yaml:/etc/otel/config.yaml
    ports:
      - "4317:4317"  # This port listens for your app's data

Now, what’s inside otel-collector-config.yaml?

receivers:
  otlp:
    protocols:
      grpc:

exporters:
  otlp:
    endpoint: "last9-endpoint:4317"  # Replace with your actual endpoint
    headers:
      api-key: "your-api-key"        # Put your API key here

service:
  pipelines:
    logs:
      receivers: [otlp]
      exporters: [otlp]

This tells the Collector: “Hey, accept logs coming in over gRPC, and send them on to Last9.”

Step 2: Send Logs From Your .NET App Using Serilog

Once the Collector is ready, configure Serilog in your app to send logs to it. Add this to your Serilog setup:

.WriteTo.OpenTelemetry(options =>
{
    options.Endpoint = "http://localhost:4317";  // Pointing to the Collector
    options.Protocol = OtlpProtocol.Grpc;         // Use gRPC to send logs
});

What this does - Serilog keeps making logs look nice and structured. Now, it also sends them to the Collector, which then forwards them. The logs include helpful info like trace IDs, so you can connect a log to the trace it belongs to.

Step 3: Add Traces and Metrics (If Needed)

You can also get OpenTelemetry to collect traces and metrics with just a few lines. It catches common things like HTTP requests and response times without extra coding.

In your app’s startup:

builder.Services.AddOpenTelemetryTracing(b =>
{
    b.AddAspNetCoreInstrumentation()
     .AddHttpClientInstrumentation()
     .AddOtlpExporter(o => o.Endpoint = new Uri("http://localhost:4317"));
});

builder.Services.AddOpenTelemetryMetrics(b =>
{
    b.AddAspNetCoreInstrumentation()
     .AddOtlpExporter(o => o.Endpoint = new Uri("http://localhost:4317"));
});

This hooks up tracing and metrics, sending them to the same Collector.

What You Need to Install

Make sure you have these packages:

dotnet add package Serilog.Sinks.OpenTelemetry
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http

A Quick Config Example

If you use a config file instead of code, here’s what it looks like in appsettings.json:

{
  "Serilog": {
    "Using": [ "Serilog.Sinks.OpenTelemetry" ],
    "MinimumLevel": "Information",
    "WriteTo": [
      {
        "Name": "OpenTelemetry",
        "Args": {
          "endpoint": "http://localhost:4317",
          "protocol": "grpc"
        }
      }
    ],
    "Enrich": [ "FromLogContext", "WithSpan" ]
  }
}

Environment Variables You Can Use

Sometimes you want to tweak without touching code:

  • OTEL_EXPORTER_OTLP_ENDPOINT — where your app sends data.
  • OTEL_RESOURCE_ATTRIBUTES — add tags like service.name=yourapp,environment=prod.

Why This Setup Works

Logs are no longer floating alone. They come with trace info, making debugging way easier. You can follow a user request from start to finish, logs, metrics, and traces all lined up.

💡
If you're weighing observability tools, this comparison of CloudWatch and OpenTelemetry breaks down the trade-offs and helps you decide what fits your stack.

One of the biggest wins with Serilog OpenTelemetry integration is the ability to connect your logs with traces. Instead of looking at logs or traces as separate puzzles, you get a clear, combined picture.

Here are some easy ways to add trace context to your logs:

Correlating Logs with Trace Context

You’ve got two main choices to tie trace IDs into your logs: manual or automatic.

1. Manual Correlation

Here’s a simple way to pull the current trace context and include it in a log:

var activity = Activity.Current;
if (activity != null)
{
    _logger.Information("Processing request {TraceId} {SpanId}", activity.TraceId, activity.SpanId);
}

This works fine for small cases, but it means sprinkling this code everywhere you want trace info.

2. Automatic Correlation with Serilog Enrichers

Want to skip repeating yourself? Use Serilog.Enrichers.Span it automatically adds trace context to every log entry.

dotnet add package Serilog.Enrichers.Span

Then update your Serilog config:

Log.Logger = new LoggerConfiguration()
    .Enrich.WithSpan()
    .WriteTo.OpenTelemetry(/* your config */)
    .CreateLogger();

Now, every log includes trace and span IDs without extra code. This makes logs and traces easy to connect when you’re troubleshooting production issues.

Writing Logs as Trace Events for a Unified View

You can write logs directly into OpenTelemetry trace spans as events. That way, when you look at a trace in your observability tool, you’ll see logs inline with the timeline.

Here’s a simple custom Serilog sink to do this:

public class TraceEventSink : ILogEventSink
{
    public void Emit(LogEvent logEvent)
    {
        var activity = Activity.Current;
        if (activity != null)
        {
            var attributes = logEvent.Properties.ToDictionary(
                prop => prop.Key,
                prop => (object)prop.Value.ToString());

            activity.AddEvent(new ActivityEvent(
                logEvent.RenderMessage(),
                DateTimeOffset.Now,
                new ActivityTagsCollection(attributes)));
        }
    }
}

Add it to Serilog like this:

Log.Logger = new LoggerConfiguration()
    .WriteTo.Sink(new TraceEventSink())
    .WriteTo.OpenTelemetry()
    .CreateLogger();

With this setup, your logs show up in both places:

  • Your usual logs backend
  • Inside trace spans, giving a step-by-step story of what happened during a request
💡
Using Serilog with OpenTelemetry? You’ll also want to understand the role of the OpenTelemetry Collector vs Exporter and how each fits into your pipeline.

How to Minimize Overhead in Production Telemetry

While Serilog and OpenTelemetry are powerful tools, using them in production environments requires some care, especially in systems with high throughput or strict latency budgets.

Unbounded logging, full trace capture, or aggressive flush intervals can add unnecessary load. It’s important to strike a balance between visibility and performance.

Here are a few key areas to optimize:

Key Considerations

  • Log Levels
    Use appropriate log levels. Keep Information for key events and Debug limited to local or test environments. Avoid overly verbose logging in production.
  • Batching
    Batching helps reduce the overhead of frequent exports. Configure exporters to batch data and flush at regular intervals instead of sending each event individually.
  • Sampling
    For traces, consider probabilistic sampling. Capturing every trace might be useful in dev, but it doesn’t scale. Sampling ensures you retain enough context without overwhelming your backend.
  • Buffering
    Exporters should be configured with adequate buffers to handle spikes. Under-provisioned buffers can lead to dropped data under load.
💡
Understanding how histograms work in OpenTelemetry can help you capture latency and performance patterns more effectively.

Optimizing for High-Traffic Systems

In applications with high request volumes, default configurations often fall short. Consider:

  • Trace Sampling: A simple example using ratio-based sampling:
builder.Services.AddOpenTelemetryTracing(b => {
    b.SetSampler(new TraceIdRatioBasedSampler(0.1)); // 10% of traces
    // other configuration
});

This reduces trace volume while still providing useful insights.

  • Log Filtering: Introduce filters to discard low-signal logs in production. Focus on logs that provide actionable value.
  • Tuning Batch Settings: Adjust batch size and flush intervals to suit your traffic profile. Too frequent flushing adds overhead; too infrequent risks delay or data loss during shutdowns.
💡
Now, fix production .NET log issues instantly—right from your IDE, with AI and Last9 MCP. Bring real-time production context—logs, metrics, and traces—into your

Practical Patterns for Using Serilog with OpenTelemetry in Production

Below are two patterns that can make a real impact in production systems.

Add Context to Logs Without Repeating Yourself

Logs are far more useful when they include context like OrderId, CustomerId, or UserId. But you don’t want to repeat those in every log line manually.

Instead, use Serilog’s contextual logging to attach properties that automatically apply to all logs within a specific scope:

using Serilog.Context;

using (LogContext.PushProperty("OrderId", orderId))
using (LogContext.PushProperty("CustomerId", customerId))
{
    _logger.Information("Processing order");
    // Every log here will include OrderId and CustomerId automatically
}

This pattern is especially useful in request pipelines, background workers, or anything tied to a logical unit of work. It makes filtering and debugging much easier, without adding noise to every log call.

Route Critical and Non-Critical Logs Separately

Not all logs are equal. Some deserve to be retained longer or sent to a more robust backend, while others are mainly for short-term visibility or debugging.

You can configure Serilog to route logs by severity, using different filters and exporters for each stream:

Log.Logger = new LoggerConfiguration()
    .WriteTo.Logger(lc => lc
        .Filter.ByIncludingOnly(e => e.Level >= LogEventLevel.Error)
        .WriteTo.OpenTelemetry(/* high-priority sink */))
    .WriteTo.Logger(lc => lc
        .Filter.ByIncludingOnly(e => e.Level < LogEventLevel.Error)
        .WriteTo.OpenTelemetry(/* lower-priority sink */))
    .CreateLogger();

This gives you more control over log storage, cost, and noise. For example, you might keep critical errors in a long-retention backend, while routing info/debug logs to a cheaper, short-term store, or even dropping them in high-traffic paths.

Conclusion

Tying Serilog and OpenTelemetry together gives you more than just logs or traces; it gives you connected, meaningful context across your application.

To get the most out of this setup, you’ll need a backend that handles logs, metrics, and traces without adding overhead. That’s where our platform, Last9, fits naturally. It works seamlessly with OpenTelemetry and lets you focus on what the data means, not how to wire it all together.

Talk to us to know more or get started for free today!

FAQs

How is Serilog different from traditional .NET logging frameworks?
Serilog uses structured logging, where log data is captured as key-value pairs instead of plain strings. This makes logs easier to query and analyze, unlike traditional tools like log4net or NLog, which rely heavily on text formatting.

Can I use Serilog with OpenTelemetry in both .NET Framework and .NET Core?
Yes. It works with both, but the setup differs slightly. .NET Core and .NET 5+ have more built-in support, while .NET Framework may require additional compatibility packages.

Does OpenTelemetry add overhead to my application?
When configured correctly—with batching and sampling—the performance overhead is minimal (typically 1–3%). For most applications, the added visibility far outweighs the cost.

How can I filter or redact sensitive data from logs before export?
You can use Serilog’s filtering features to exclude sensitive content before it’s sent out:

.WriteTo.OpenTelemetry(options => {
    options.IncludeFormattedMessage = true;
    options.PreFilter = logEvent =>
        !logEvent.Properties.TryGetValue("Contains", out var val) ||
        !val.ToString().Contains("password");
})

This ensures fields like passwords or tokens don’t get exported.

Can I gradually migrate to Serilog and OpenTelemetry?
You can run Serilog and your existing logging setup side by side. Start by integrating Serilog into a few components, monitor the results, and roll it out further as you gain confidence in the new system.

Contents

Do More with Less

Unlock high cardinality monitoring for your teams.