RO EN

Observability (2) — structured logging cu Serilog și Application Insights

Observability (2) — structured logging cu Serilog și Application Insights ✨ Imagine generată cu AI
Doru Bulubașa
05 august 2026
53 vizualizări

A doua parte din seria despre observabilitate. Prima parte a așezat cei trei piloni. Acum facem primul pilon serios: structured logging cu Serilog, vărsat în Application Insights.


De ce Serilog peste logging-ul built-in

ILogger din Microsoft.Extensions.Logging suportă deja template-uri structurate — și rămâne abstracția pe care o injectezi peste tot. Serilog intră dedesubt, ca provider, și aduce ce lipsește: sinks mature (Application Insights, Console, File, Seq), enrichers care adaugă context automat pe fiecare log și configurare fină per namespace, din appsettings.

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.ApplicationInsights
dotnet add package Serilog.Enrichers.Environment

Configurarea de bază

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Host.UseSerilog((context, services, configuration) => configuration
    .ReadFrom.Configuration(context.Configuration)   // niveluri din appsettings
    .ReadFrom.Services(services)
    .Enrich.FromLogContext()
    .Enrich.WithMachineName()
    .Enrich.WithProperty("Application", "chat-api")
    .Enrich.WithProperty("Version",
        Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown")
    .WriteTo.Console()
    .WriteTo.ApplicationInsights(
        services.GetRequiredService<TelemetryConfiguration>(),
        TelemetryConverter.Traces));

Detaliul important: TelemetryConverter.Traces trimite log-urile în tabela traces din Application Insights (care, cum am stabilit în partea 1, conține log-urile, nu trace-urile distribuite). Fiecare proprietate structurată devine o coloană în customDimensions — interogabilă direct în KQL.

Nivelurile din appsettings — per namespace

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft.AspNetCore": "Warning",
        "Microsoft.AspNetCore.Hosting.Diagnostics": "Warning",
        "Azure.Core": "Warning",
        "System.Net.Http.HttpClient": "Warning"
      }
    }
  }
}

Override-urile pe Microsoft.AspNetCore și Azure.Core taie zgomotul de infrastructură care altfel domină volumul (și factura). Fiind în appsettings, poți coborî temporar un namespace la Debug în producție printr-o variabilă de mediu — fără redeploy.


Structured, nu interpolat — de ce contează concret

// GRESIT -- string final, context pierdut
_logger.LogInformation($"Cache hit pentru query-ul {queryHash} al tenantului {tenantId}");

// CORECT -- proprietati structurate, interogabile
_logger.LogInformation(
    "Cache hit pentru query-ul {QueryHash} al tenantului {TenantId}",
    queryHash, tenantId);

Diferența devine vizibilă la investigație. Cu varianta structurată, în KQL poți scrie:

traces
| where customDimensions.TenantId == "tenant-42"
| where message startswith "Cache hit"
| summarize count() by bin(timestamp, 1h)

Cu varianta interpolată, aceeași întrebare înseamnă parsing de string cu regex — fragil și lent. Regula e absolută: niciodată interpolare în template-ul de log.

Obiecte întregi cu @

// Operatorul @ serializeaza obiectul in customDimensions
_logger.LogInformation("Comanda procesata: {@Order}", new
{
    order.Id,
    order.TenantId,
    ItemCount = order.Items.Count,
    order.Total
});
// Atentie: doar proprietatile relevante, niciodata entitatea completa
// (payload-uri mari = cost + risc de date sensibile in log-uri)

Context ambient cu LogContext

TenantId-ul apare în aproape fiecare log dintr-un SaaS multi-tenant. În loc să-l pasezi manual peste tot, un middleware îl împinge în LogContext — și toate log-urile din request îl primesc automat:

public class TenantLoggingMiddleware
{
    private readonly RequestDelegate _next;
    public TenantLoggingMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext context)
    {
        var tenantId = context.User.FindFirstValue("tenant_id") ?? "anonymous";

        using (LogContext.PushProperty("TenantId", tenantId))
        using (LogContext.PushProperty("CorrelationId",
            context.TraceIdentifier))
        {
            await _next(context);
        }
    }
}

// Program.cs -- inainte de endpoints
app.UseMiddleware<TenantLoggingMiddleware>();

De acum, orice _logger.LogInformation(...) din adâncul unui serviciu are TenantId în customDimensions, fără ca serviciul să știe de existența lui.


Request logging compact

ASP.NET Core loghează implicit 3-4 evenimente per request. Serilog le înlocuiește cu unul singur, dens:

// Program.cs -- dupa build
app.UseSerilogRequestLogging(options =>
{
    options.MessageTemplate =
        "HTTP {RequestMethod} {RequestPath} => {StatusCode} in {Elapsed:0.0}ms";

    options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
    {
        diagnosticContext.Set("ClientIp",
            httpContext.Connection.RemoteIpAddress?.ToString());
        diagnosticContext.Set("UserAgent",
            httpContext.Request.Headers.UserAgent.ToString());
    };

    // Nivel dinamic: erorile la Error, health checks la Verbose (taiate)
    options.GetLevel = (httpContext, elapsed, ex) => ex is not null
        ? LogEventLevel.Error
        : httpContext.Request.Path.StartsWithSegments("/health")
            ? LogEventLevel.Verbose
            : LogEventLevel.Information;
});

GetLevel cu Verbose pe /health merită subliniat: probele de liveness/readiness din Container Apps lovesc endpoint-urile la câteva secunde — fără această regulă, jumătate din log-urile tale sunt health checks.


Costul: log-urile se plătesc la GB

Application Insights taxează ingestia. Trei mecanisme de control, în ordinea în care le aplici:

  • Niveluri corecte — override-urile per namespace de mai sus; cel mai mare câștig, gratuit
  • Sampling adaptiv — la volum mare, Application Insights păstrează un procent reprezentativ; corelarea pe operație e păstrată (un request e eșantionat cu toate log-urile lui)
  • Daily cap ca plasă de siguranță — limită zilnică de ingestie; alertă când o atingi, pentru că dincolo de ea ești orb
# Daily cap pe workspace -- plasa de siguranta, nu strategie
az monitor log-analytics workspace update \
  --resource-group my-rg \
  --workspace-name my-workspace \
  --quota 5

Ce urmează

Log-urile au acum structură și context. În partea a treia le legăm între servicii: distributed tracing cu OpenTelemetry în ASP.NET Core și Azure Functions — trace id-ul care traversează HTTP, Service Bus și Cosmos DB.

Întrebări? Scrie-mi la contact@ludoprogramming.com.