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
, andTotalAmount
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.
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 likeservice.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.
How to Link Logs and Traces for Better Debugging with Serilog and OpenTelemetry
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
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. KeepInformation
for key events andDebug
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.
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.
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.