Start Debugging

Переход с Serilog на журналирование OpenTelemetry в .NET 11

Пошаговое руководство по переводу приложения на .NET 11 с Serilog на журналирование OpenTelemetry: низкорисковый мост Serilog.Sinks.OpenTelemetry, полный переход на Microsoft.Extensions.Logging, что ломается, как проверить и как откатиться.

Если ваша команда стандартизировалась на OpenTelemetry для трассировок и метрик, то выбивается из общего ряда обычно журналирование, которое по-прежнему проходит через Serilog и файловый или Seq sink. Это руководство переводит приложение на .NET 11 (OpenTelemetry .NET SDK 1.15.x, Serilog 4.x) с такой разрозненной конфигурации на журналирование OpenTelemetry. Есть два пути: мост на один вечер, который сохраняет Serilog и меняет только sink, и полный переход на Microsoft.Extensions.Logging, который полностью убирает Serilog. Мост обратим одним коммитом и почти ничего не ломает. Полная миграция занимает день или два на реальной кодовой базе, затрагивает каждый BeginScope и каждый шаблон сообщения и оправдана только в том случае, если удаление зависимости от Serilog и объединение на одном API журналирования — это реальная цель, а не просто приятное дополнение.

Зачем вообще переводить журналирование на OpenTelemetry

Если вы ещё не подключили трассировки OpenTelemetry, сделайте это сначала: использование OpenTelemetry с .NET 11 и бесплатным бэкендом описывает настройку экспортёра и бэкенда, которую это руководство предполагает уже выполненной.

Что ломается

ОбластьИзменениеСерьёзность
Конфигурация sinkФайловый/консольный/Seq sink заменяются экспортёром OTLP или Serilog.Sinks.OpenTelemetryвысокая (полный) / низкая (мост)
Статический Log.Logger + CreateBootstrapLogger()Удаляется при полной миграции; нет двухэтапного журналирования при запускевысокая (только полный)
Обогатители LogContext.PushPropertyЗаменяются на ILogger.BeginScope плюс IncludeScopes = trueсредняя (только полный)
Оператор деструктуризации {@Order}Нет эквивалента в Microsoft.Extensions.Logging; журналируйте скалярные поля или сериализуйте явносредняя (только полный)
UseSerilogRequestLogging()Заменяется инструментацией OTel в ASP.NET Core или AddHttpLoggingсредняя (только полный)
Блок конфигурации MinimumLevelПереходит в секцию Logging:LogLevel в appsettings.jsonнизкая (только полный)
Названия уровней серьёзностиSerilog Verbose отображается на OTel Trace; Information остаётсянизкая

Столбец “мост” важен: если вы выбираете путь A, применяется только первая строка, и серьёзность низкая. Всё остальное — забота полной миграции.

Предполётный чек-лист

Шаги миграции

1. Зафиксируйте версии пакетов

Определите путь, затем установите соответствующие пакеты. Оба пути нацелены на одну и ту же линейку 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" />

Проверка: dotnet restore проходит успешно и dotnet build чист до того, как вы измените какой-либо код.

2a. Путь A — замена Serilog sink на OTLP

Это низкорисковый путь. Сохраните каждый обогатитель, шаблон сообщения и вызов LogContext, которые у вас уже есть. Замените только конфигурацию 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();

Sink читает Activity.Current и автоматически внедряет TraceId и SpanId в каждую запись журнала (IncludedData.TraceIdField и SpanIdField включены по умолчанию), поэтому корреляция между сигналами работает без дополнительного обогатителя. Ваши шаблоны _logger.LogInformation("Placed order {OrderId}", id) проходят без изменений.

Проверка: запустите приложение, выполните один запрос и убедитесь, что строка журнала появляется в вашем бэкенде OTLP с тем же TraceId, что и у трассировки запроса.

2b. Путь B — переход на Microsoft.Extensions.Logging

Удалите UseSerilog() / AddSerilog() и инициализацию Log.Logger из Program.cs. Подключите OpenTelemetry к встроенному построителю журналирования.

// .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();

Если ваше приложение уже вызывает builder.Services.AddOpenTelemetry() для трассировок и метрик, предпочтите единую форму, чтобы все три сигнала разделяли один ресурс и экспортёр:

// .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() (добавлен в SDK 1.8) регистрирует экспортёр OTLP сразу для каждого сигнала, поэтому вам не нужно повторять конечную точку три раза.

Проверка: dotnet run, затем убедитесь, что строка ILogger достигает бэкенда с правильным service.name и заполненным телом.

3. Перевод обогатителей в области (только путь B)

У LogContext.PushProperty("UserId", id) из Serilog нет эквивалента в Microsoft.Extensions.Logging. Используйте ILogger.BeginScope, и свойства попадут в запись OTLP, потому что вы установили 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");
}

Проверка: испущенная запись журнала несёт UserId как атрибут. Если он отсутствует, вы забыли IncludeScopes = true.

4. Замена журналирования запросов (только путь B)

app.UseSerilogRequestLogging() производил одну сводную строку журнала на запрос. С OpenTelemetry инструментация ASP.NET Core уже испускает HTTP-серверный span на каждый запрос, что является лучшим примитивом для вопроса “что произошло в этом запросе”. Если вам всё же нужна строка журнала, добавьте HTTP-журналирование:

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

Проверка: каждый запрос производит HTTP-серверный span (и опциональную запись HTTP-журнала), коррелированный по TraceId.

5. Перенос конфигурации уровней в appsettings

Блок MinimumLevel из Serilog заменяется стандартной секцией Logging:LogLevel, которую провайдер OpenTelemetry учитывает как любой другой.

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

Проверка: установите категорию в Warning, убедитесь, что её строки Information перестают появляться в бэкенде.

Чек-лист проверки

Запустите его после любого пути, прежде чем удалять старые пакеты:

План отката

Путь A обратим одним коммитом. Верните блок WriteTo.OpenTelemetry(...) обратно к вашей старой конфигурации WriteTo.Seq(...) / WriteTo.File(...) и удалите пакет Serilog.Sinks.OpenTelemetry. Ничего больше не изменилось, поэтому поверхности риска нет.

Путь B обратим, но не тривиально. Держите пакеты Serilog установленными, а старую инициализацию Program.cs — в истории git до тех пор, пока новый конвейер не отработает в продакшене один цикл релиза. Если вам нужно откатиться, восстановите UseSerilog(), инициализацию Log.Logger и преобразуйте вызовы BeginScope обратно в LogContext.PushProperty. Поскольку переход затрагивает области и журналирование запросов по всей кодовой базе, относитесь к откату как к собственной небольшой миграции, а не к однострочному переключателю. Не удаляйте пакеты Serilog из .csproj, пока проверка не пройдёт в продакшене.

Подводные камни, на которые мы наткнулись

Пустые тела журналов в бэкенде. Если IncludeFormattedMessage оставлен в значении по умолчанию false, запись OTLP отправляется со структурированными атрибутами, но без отрендеренного сообщения, и некоторые бэкенды показывают пустую строку. Включите его. Сочетайте его с ParseStateValues = true, чтобы именованные плейсхолдеры ({OrderId}) также попадали как атрибуты, а не только внутрь форматированной строки.

Свойства областей исчезают. IncludeScopes по умолчанию равен false. Каждое значение BeginScope, а также всё, что ASP.NET Core помещает в область запроса, отбрасывается, пока вы не включите его. Это самый частый отчёт “моя миграция потеряла половину контекста журнала”.

Нет деструктуризации {@Object}. _logger.LogInformation("Got {@Order}", order) из Serilog сериализовал весь объект. Microsoft.Extensions.Logging трактует @ как литеральный текст. Журналируйте скалярные поля, по которым вы действительно делаете запросы, или сериализуйте явно с помощью System.Text.Json. Сброс целых объектов также взрывает кардинальность атрибутов, за которую некоторые бэкенды берут плату.

Потеря двухэтапного журналирования при запуске. CreateBootstrapLogger() из Serilog захватывал сбои, происходящие до построения хоста. Провайдер OpenTelemetry существует только после builder.Build(), поэтому самые ранние исключения при запуске уходят только в консоль. Если наблюдаемость на самом раннем этапе запуска важна, сохраните минимальный консольный логгер на это время.

Сюрпризы при отображении серьёзности. Serilog Verbose становится OTel Trace, а Fatal становится Critical. Если вы фильтруете или оповещаете по названиям серьёзности ниже по потоку, обновите эти правила. Debug, Information, Warning и Error отображаются один к одному.

Если вы всё равно приводите журналирование в порядок, стоит пересмотреть то, как вы испускаете структурированные данные в первую очередь; настройка структурированного журналирования с Serilog и Seq в .NET 11 описывает шаблоны сообщений, которые чисто переносятся в ILogger, а нативная трассировка OpenTelemetry в ASP.NET Core 11 объясняет, почему вам могут не понадобиться дополнительные пакеты инструментации, как только вы перейдёте на единый конвейер. Для фоновых заданий и хостируемых сервисов мониторинг фоновых заданий без Hangfire показывает ту же корреляцию, работающую вне пути запроса, а уведомление о baggage в OpenTelemetry для Aspire 13.2.4 — это напоминание держать пакеты OTel пропатченными.

Выбирайте мост, если только удаление Serilog не является реальной целью. Он даёт вам журналы OTLP и корреляцию с трассировкой уже сегодня вечером, а полный переход вы сможете выполнить позже, когда у вас будет спокойный спринт, чтобы как следует перевести области и журналирование запросов.

Источники

Comments

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

< Назад