Start Debugging

Migrar de Serilog a logging con OpenTelemetry en .NET 11

Una guía paso a paso para sacar a una app .NET 11 de Serilog y llevarla a logging con OpenTelemetry: el puente de bajo riesgo Serilog.Sinks.OpenTelemetry, el cambio completo a Microsoft.Extensions.Logging, qué se rompe, cómo verificar y cómo revertir.

Si tu equipo se estandarizó en OpenTelemetry para trazas y métricas, el que suele quedar fuera es el logging, que sigue pasando por Serilog y un sink de archivo o Seq. Esta guía mueve una app .NET 11 (OpenTelemetry .NET SDK 1.15.x, Serilog 4.x) fuera de esa configuración dividida y la lleva a logging con OpenTelemetry. Hay dos rutas: un puente de una sola tarde que mantiene Serilog y solo cambia el sink, y un cambio completo a Microsoft.Extensions.Logging que elimina Serilog por completo. El puente es reversible en un solo commit y casi no rompe nada. La migración completa toma un día o dos en una base de código real, toca cada BeginScope y cada plantilla de mensaje, y vale la pena solo si eliminar la dependencia de Serilog y unificar en una sola API de logging es un objetivo real y no algo deseable.

Por qué mover el logging a OpenTelemetry

Si todavía no cableaste las trazas de OpenTelemetry, haz eso primero: usar OpenTelemetry con .NET 11 y un backend gratuito cubre la configuración del exportador y del backend que esta guía asume que ya está en su lugar.

Qué se rompe

ÁreaCambioSeveridad
Configuración del sinkLos sinks de archivo/consola/Seq se reemplazan por el exportador OTLP o Serilog.Sinks.OpenTelemetryalta (completa) / baja (puente)
Log.Logger estático + CreateBootstrapLogger()Eliminado en la migración completa; sin logging de arranque en dos etapasalta (solo completa)
Enrichers de LogContext.PushPropertyReemplazados por ILogger.BeginScope más IncludeScopes = truemedia (solo completa)
Operador de destructuring {@Order}Sin equivalente en Microsoft.Extensions.Logging; registra campos escalares o serializa explícitamentemedia (solo completa)
UseSerilogRequestLogging()Reemplazado por la instrumentación OTel de ASP.NET Core o AddHttpLoggingmedia (solo completa)
Bloque de configuración MinimumLevelSe mueve a la sección Logging:LogLevel en appsettings.jsonbaja (solo completa)
Nombres de severidadEl Verbose de Serilog mapea a Trace de OTel; Information se queda igualbaja

La columna “puente” importa: si tomas la ruta A, solo aplica la primera fila y la severidad es baja. Todo lo demás es asunto de la migración completa.

Lista de verificación previa

Pasos de la migración

1. Fija las versiones de los paquetes

Decide la ruta y luego instala los paquetes correspondientes. Ambas rutas apuntan a la misma línea del SDK.

<!-- .NET 11, both routes -->
<!-- Route A (bridge): keep Serilog, swap the sink -->
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.OpenTelemetry" Version="4.2.0" />

<!-- Route B (full): Microsoft.Extensions.Logging + OpenTelemetry -->
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />

Verifica: dotnet restore tiene éxito y dotnet build está limpio antes de que cambies cualquier código.

2a. Ruta A — cambia el sink de Serilog por OTLP

Este es el camino de bajo riesgo. Mantén cada enricher, plantilla de mensaje y llamada a LogContext que ya tengas. Reemplaza solo la configuración del sink.

// .NET 11, C# 14, Serilog 4.x, Serilog.Sinks.OpenTelemetry 4.2.0
using Serilog;
using Serilog.Sinks.OpenTelemetry;

Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()
    .WriteTo.OpenTelemetry(options =>
    {
        options.Endpoint = "http://localhost:4318/v1/logs";
        options.Protocol = OtlpProtocol.HttpProtobuf;
        options.ResourceAttributes = new Dictionary<string, object>
        {
            ["service.name"] = "orders-api",
            ["service.version"] = "2.4.0"
        };
    })
    .CreateLogger();

El sink lee Activity.Current e inyecta TraceId y SpanId en cada registro de log automáticamente (IncludedData.TraceIdField y SpanIdField están activados por defecto), así que la correlación entre señales funciona sin un enricher extra. Tus plantillas _logger.LogInformation("Placed order {OrderId}", id) fluyen sin cambios.

Verifica: inicia la app, haz una solicitud y confirma que la línea de registro aparece en tu backend OTLP con el mismo TraceId que la traza de la solicitud.

2b. Ruta B — cambia a Microsoft.Extensions.Logging

Quita UseSerilog() / AddSerilog() y el bootstrap de Log.Logger de Program.cs. En su lugar, cablea OpenTelemetry en el constructor de logging integrado.

// .NET 11, C# 14, OpenTelemetry .NET SDK 1.15.3
using OpenTelemetry.Logs;
using OpenTelemetry.Resources;

var builder = WebApplication.CreateBuilder(args);

builder.Logging.AddOpenTelemetry(options =>
{
    options.IncludeScopes = true;            // keep BeginScope properties
    options.IncludeFormattedMessage = true;  // populate the log body
    options.ParseStateValues = true;         // capture structured attributes
    options.SetResourceBuilder(
        ResourceBuilder.CreateDefault()
            .AddService("orders-api", serviceVersion: "2.4.0"));
    options.AddOtlpExporter(o =>
    {
        o.Endpoint = new Uri("http://localhost:4318");
    });
});

var app = builder.Build();

Si tu app ya llama a builder.Services.AddOpenTelemetry() para trazas y métricas, prefiere la forma unificada para que las tres señales compartan un solo recurso y exportador:

// .NET 11, OpenTelemetry .NET SDK 1.15.3
builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService("orders-api", serviceVersion: "2.4.0"))
    .WithTracing(t => t.AddAspNetCoreInstrumentation())
    .WithMetrics(m => m.AddAspNetCoreInstrumentation())
    .UseOtlpExporter(); // one call: configures OTLP for traces, metrics, AND logs

// Logs still need the provider on the logging builder:
builder.Logging.AddOpenTelemetry(o =>
{
    o.IncludeScopes = true;
    o.IncludeFormattedMessage = true;
    o.ParseStateValues = true;
});

UseOtlpExporter() (agregado en el SDK 1.8) registra el exportador OTLP para cada señal a la vez, así que no repites el endpoint tres veces.

Verifica: dotnet run, luego confirma que una línea de ILogger llega al backend con el service.name correcto y un cuerpo poblado.

3. Traduce los enrichers a scopes (solo Ruta B)

El LogContext.PushProperty("UserId", id) de Serilog no tiene equivalente en Microsoft.Extensions.Logging. Usa ILogger.BeginScope y las propiedades fluyen al registro OTLP porque pusiste IncludeScopes = true.

// Before (Serilog)
using (LogContext.PushProperty("UserId", userId))
{
    _logger.LogInformation("Loaded cart");
}

// After (.NET 11, Microsoft.Extensions.Logging)
using (_logger.BeginScope(new Dictionary<string, object> { ["UserId"] = userId }))
{
    _logger.LogInformation("Loaded cart");
}

Verifica: el registro de log emitido lleva UserId como atributo. Si falta, olvidaste IncludeScopes = true.

4. Reemplaza el logging de solicitudes (solo Ruta B)

app.UseSerilogRequestLogging() producía una línea de registro resumida por solicitud. Con OpenTelemetry, la instrumentación de ASP.NET Core ya emite un span de servidor HTTP por solicitud, que es la primitiva mejor para “qué pasó en esta solicitud”. Si aún quieres una línea de registro, agrega logging de HTTP:

// .NET 11
builder.Services.AddHttpLogging(o => { });
// ...
app.UseHttpLogging();

Verifica: cada solicitud produce un span de servidor HTTP (y una entrada de log HTTP opcional) correlacionados por TraceId.

5. Mueve la configuración de nivel a appsettings

El bloque MinimumLevel de Serilog se reemplaza por la sección estándar Logging:LogLevel, que el proveedor de OpenTelemetry respeta como cualquier otro.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

Verifica: pon una categoría en Warning, confirma que sus líneas de Information dejan de aparecer en el backend.

Lista de verificación

Ejecuta esto después de cualquiera de las rutas, antes de que borres los paquetes viejos:

Plan de reversión

La Ruta A es reversible en un solo commit. Revierte el bloque WriteTo.OpenTelemetry(...) a tu vieja configuración WriteTo.Seq(...) / WriteTo.File(...) y quita el paquete Serilog.Sinks.OpenTelemetry. Nada más cambió, así que no hay superficie de riesgo.

La Ruta B es reversible pero no trivial. Mantén los paquetes de Serilog instalados y el viejo bootstrap de Program.cs en tu historial de git hasta que la nueva tubería haya corrido en producción durante un ciclo de versión. Si necesitas revertir, restaura UseSerilog(), el bootstrap de Log.Logger y convierte las llamadas a BeginScope de vuelta a LogContext.PushProperty. Como el cambio toca scopes y logging de solicitudes a lo largo de la base de código, trata la reversión como su propia pequeña migración, no como un interruptor de una línea. No borres los paquetes de Serilog del .csproj hasta que la verificación haya pasado en producción.

Problemas con los que nos topamos

Cuerpos de log vacíos en el backend. Si IncludeFormattedMessage se queda en su valor por defecto false, el registro OTLP se envía con atributos estructurados pero sin mensaje renderizado, y algunos backends muestran una línea en blanco. Actívalo. Combínalo con ParseStateValues = true para que los placeholders con nombre ({OrderId}) también lleguen como atributos en lugar de solo dentro de la cadena formateada.

Las propiedades de scope desaparecen. IncludeScopes tiene por defecto false. Cada valor de BeginScope, y cualquier cosa que ASP.NET Core ponga en el scope de la solicitud, se descarta hasta que lo habilites. Este es el reporte de “mi migración perdió la mitad de mi contexto de log” más común.

Sin destructuring {@Object}. El _logger.LogInformation("Got {@Order}", order) de Serilog serializaba el objeto completo. Microsoft.Extensions.Logging trata @ como texto literal. Registra los campos escalares que de verdad consultas, o serializa explícitamente con System.Text.Json. Volcar objetos completos también explota la cardinalidad de los atributos, por la cual algunos backends cobran.

Perder el logging de arranque en dos etapas. El CreateBootstrapLogger() de Serilog capturaba fallos que ocurren antes de que el host se construya. El proveedor de OpenTelemetry solo existe después de builder.Build(), así que las excepciones de arranque muy tempranas van solo a la consola. Si la observabilidad del arranque temprano importa, mantén un logger de consola mínimo para esa ventana.

Sorpresas en el mapeo de severidad. El Verbose de Serilog se convierte en Trace de OTel, y Fatal se convierte en Critical. Si filtras o alertas con nombres de severidad aguas abajo, actualiza esas reglas. Debug, Information, Warning y Error mapean uno a uno.

Si de todas formas estás afinando el logging, vale la pena revisar cómo emites datos estructurados en primer lugar; configurar logging estructurado con Serilog y Seq en .NET 11 cubre los patrones de plantillas de mensaje que se trasladan limpiamente a ILogger, y el tracing nativo de OpenTelemetry de ASP.NET Core 11 explica por qué quizás no necesites paquetes de instrumentación extra una vez que estás en la tubería unificada. Para trabajos en segundo plano y servicios alojados, monitorear trabajos en segundo plano sin Hangfire muestra la misma correlación funcionando fuera de la ruta de la solicitud, y el aviso sobre baggage de OpenTelemetry de Aspire 13.2.4 es un recordatorio para mantener los paquetes de OTel parcheados.

Elige el puente a menos que eliminar Serilog sea un objetivo real. Te da registros OTLP y correlación de trazas esta misma noche, y puedes hacer el cambio completo más tarde cuando tengas un sprint tranquilo para traducir scopes y logging de solicitudes como es debido.

Fuentes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Volver