← Back to all posts

OpenTelemetry with Serilog and Azure Application Insights in .NET

OpenTelemetry with Serilog and Azure Application Insights in .NET

Reading Time: 5 minutes

Problem

Most .NET teams eventually hit the same observability wall:

  • Traces exist in one place, logs in another, and metrics in a third
  • Log messages are inconsistent and hard to query
  • Production incidents take too long to diagnose because correlation is weak
  • The default logging setup is often “good enough” early on, then painful at scale

Without a clear strategy, your telemetry becomes noisy instead of actionable. You can have data and still not have answers.

Solution

Use a combined approach:

  • OpenTelemetry for vendor-neutral tracing and metrics
  • Serilog for structured, query-friendly logs with strong enrichment support
  • Azure Application Insights as the operational backend for monitoring and investigation

This setup gives you consistent telemetry with correlation across request traces, dependencies, exceptions, and logs.

Why Serilog specifically (vs not using Serilog)?

  1. Better structured logging ergonomics: message templates and property capture are first-class and developer-friendly.
  2. Richer enrichment pipeline: add request IDs, tenant IDs, user IDs, machine info, and custom business context consistently.
  3. Flexible sink strategy: write to console, files, and App Insights simultaneously without custom plumbing.
  4. Cleaner queryability: structured fields land as searchable properties, which makes KQL queries much more effective.
  5. Predictable formatting and output control: useful for local debugging and production diagnostics.

Can you run OpenTelemetry + App Insights without Serilog? Yes. But teams often lose logging consistency and enrichment quality over time, which slows incident response.

Description

1) Install packages

dotnet add package Azure.Monitor.OpenTelemetry.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.ApplicationInsights
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Process
dotnet add package Serilog.Enrichers.Thread

2) Configure OpenTelemetry + Serilog in Program.cs

using Azure.Monitor.OpenTelemetry.AspNetCore;
using Microsoft.ApplicationInsights.Extensibility;
using Serilog;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetry()
    .UseAzureMonitor(options =>
    {
        options.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
    });

builder.Host.UseSerilog((context, services, configuration) =>
{
    var telemetryConfiguration = services.GetRequiredService<TelemetryConfiguration>();

    configuration
        .ReadFrom.Configuration(context.Configuration)
        .ReadFrom.Services(services)
        .Enrich.FromLogContext()
        .Enrich.WithMachineName()
        .Enrich.WithProcessId()
        .Enrich.WithThreadId()
        .WriteTo.Console()
        .WriteTo.ApplicationInsights(telemetryConfiguration, TelemetryConverter.Traces);
});

var app = builder.Build();

app.UseSerilogRequestLogging();

app.MapGet("/orders/{orderId}", (string orderId, ILogger<Program> logger) =>
{
    logger.LogInformation("Fetching order {OrderId}", orderId);

    // Simulated logic
    if (orderId == "404")
    {
        logger.LogWarning("Order {OrderId} was not found", orderId);
        return Results.NotFound();
    }

    return Results.Ok(new { id = orderId, status = "Processing" });
});

app.Run();

3) Add minimum config in appsettings.json

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId", "WithProcessId"]
  }
}

Set APPLICATIONINSIGHTS_CONNECTION_STRING as an environment variable (local secrets and Azure App Service/Container App settings in production).

4) What improves in practice

  • A single request can be followed across inbound HTTP, outbound dependency calls, and related logs.
  • Log properties such as OrderId, TenantId, and CorrelationId become first-class query fields.
  • Teams can answer “what failed, where, and for whom” much faster during incidents.

5) Example Application Insights log queries

Once Serilog is sending structured logs into Application Insights, you can query them with Kusto Query Language (KQL).

Find recent warning and error logs:

traces
| where timestamp > ago(1h)
| where severityLevel >= 2
| project timestamp, severityLevel, message, operation_Id, customDimensions
| order by timestamp desc

Find all logs for a specific order ID captured by Serilog:

traces
| where timestamp > ago(24h)
| where customDimensions.OrderId == "12345"
| project timestamp, message, severityLevel, operation_Id
| order by timestamp desc

Find failed requests and join them to related logs using the operation ID:

requests
| where timestamp > ago(24h)
| where success == false
| project requestTimestamp = timestamp, name, resultCode, operation_Id, duration
| join kind=leftouter (
  traces
  | project traceTimestamp = timestamp, operation_Id, message, severityLevel
) on operation_Id
| order by requestTimestamp desc, traceTimestamp desc

Find exceptions together with the related structured log context:

exceptions
| where timestamp > ago(24h)
| project exceptionTimestamp = timestamp, type, outerMessage, operation_Id
| join kind=leftouter (
  traces
  | project traceTimestamp = timestamp, operation_Id, message, customDimensions
) on operation_Id
| order by exceptionTimestamp desc, traceTimestamp desc

Filter by a specific tenant or correlation ID when Serilog enrichers push those values:

traces
| where timestamp > ago(4h)
| where customDimensions.TenantId == "tenant-a"
   or customDimensions.CorrelationId == "7f1c1a4e-8e2d-4ea7-9d95-2fd7e7c2b111"
| project timestamp, message, severityLevel, customDimensions
| order by timestamp desc

This is where Serilog pays off. Without structured properties, you are usually reduced to brittle text matching. With Serilog, fields such as OrderId, TenantId, and CorrelationId become reliable query inputs instead of substrings buried inside log messages.

Serilog vs no Serilog (quick comparison)

CapabilityOpenTelemetry + default logging onlyOpenTelemetry + Serilog
Structured logging disciplinePossible, but often inconsistent by teamStrong and consistent via templates/enrichers
Context enrichmentBasic/manualExtensive and reusable
Multi-destination loggingMore setup effortStraightforward with sinks
Local readability + production queryabilityVaries by provider/configPredictable and configurable
Incident debugging speedGood baselineTypically faster due to richer context

Summary

OpenTelemetry gives you the telemetry standard. Azure Application Insights gives you the monitoring platform. Serilog gives you high-quality structured logs that are easier to correlate, query, and trust.

If your team is serious about reducing time-to-diagnose in production, this combination is a practical, low-friction upgrade from basic logging.

References