Files
Deal/src/core/Deal.Api/Logging/DealLogging.cs
T
Rustam Khalimov 410194b0cb Разбить Infrastructure и корень Deal.Api по назначению
Integrations -> Abstractions/Exceptions/Extensions/Models/Options/
Services (включая Storage); Persistence-конфигурации -> Configurations;
корень Deal.Api (оркестратор/планировщики/DTO) -> Services/Dtos.
namespace приведён к путям, using добавлены/дедуплицированы, FQN
обновлены.
2026-09-11 13:20:10 +03:00

127 lines
8.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Extensions.Configuration;
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Compact;
using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Logging;
// Serilog-конфигурация процесса core (Ruling 7/9, план Task 14).
// Все четыре процесса (core + telegram/ai/ml-сервисы) логируют через Serilog: консоль — JSON в
// prod-стиле (CompactJsonFormatter: одна JSON-строка на событие, поля @t/@mt/@l — парсинг
// Loki/Promtail) либо текст в Development; плюс rolling-файл data/logs/deal-<процесс>.json
// (под ContentRoot; у core каталог data смонтирован volume-ом, compose.prod). Уровень/каталог
// переопределяются env: DEAL_LOG_LEVEL, DEAL_LOGS_DIR.
// Конфигурация кодом, а не секцией appsettings: у трёх сервисов appsettings.json нет (весь конфиг —
// env, Ruling 13), поэтому единый для четырёх хостов код-набор с env-переопределениями дешевле и не
// расходится между процессами. Правило «секреты не логируются» (Ruling 13) соблюдается на уровне
// сообщений (в лог-конфигурации секретов нет; запрос-логи Task 14 логируют метод/путь/статус без
// query/заголовков/тел). OTel-метрики/трейсы и Prometheus в этапе 7 не добавляются (Ruling 7) —
// стек: Serilog-логи → docker-логи → Promtail → Loki → Grafana.
// Вызов — из Program.cs процесса (entry point): DealLogging.Configure(builder, "…") ДО
// builder.Build(). Интеграционные тесты сервисов поднимают хост через *ServiceHost.Create
// БЕЗ этого вызова (конфигурация логирования — забота production-точки входа), поэтому тесты не пишут
// файлы-логи и не меняют своё логирование.
internal static class DealLogging
{
// Env-ключ минимального уровня Serilog (Debug/Information/Warning/Error; дефолт Information).
private const string MinimumLevelEnvKey = "DEAL_LOG_LEVEL";
// Env-ключ каталога rolling-файлов (дефолт data/logs под ContentRoot).
private const string LogsDirectoryEnvKey = "DEAL_LOGS_DIR";
// Каталог логов по умолчанию (относительно ContentRoot): core — внутри volume /app/data.
private const string DefaultLogsSubdirectory = "data/logs";
// Шаблон имени rolling-файла (Serilog добавляет дату перед расширением): deal-core-20260908.json.
private const string LogFileNameTemplate = "deal-{0}-.json";
// Сколько rolling-файлов хранится (суток); старшие удаляются Serilog автоматически.
private const int RetainedFileCount = 30;
// Текстовая разметка консоли в Development (цвета — дефолтной темой Serilog).
private const string DevelopmentConsoleTemplate =
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}";
// Категория EF Core: команды SQL логируются на Warning+ (шум запросов не попадает в Loki).
private const string EntityFrameworkCoreCategory = "Microsoft.EntityFrameworkCore";
// Категория Grpc.AspNetCore: не ниже Information (внутренние Debug-события вызовов не дублируют access-лог).
private const string GrpcCategory = "Grpc";
// Дефолтный уровень при пустом/невалидном env DEAL_LOG_LEVEL.
private const LogEventLevel DefaultMinimumLevel = LogEventLevel.Information;
/// <summary>
/// Подключает Serilog к хосту (builder.Host.UseSerilog). Регистрация отложенная: конфигурация
/// логгера применяется при <c>builder.Build()</c>, когда среда/конфигурация (env) уже собраны.
/// </summary>
/// <param name="builder">Билдер WebApplication процесса (до Build).</param>
/// <param name="processName">Имя процесса для имени файла-лога (core/telegram/ai/ml).</param>
public static void Configure(WebApplicationBuilder builder, string processName)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrWhiteSpace(processName);
builder.Host.UseSerilog((context, loggerConfiguration) =>
Apply(loggerConfiguration, context.HostingEnvironment, context.Configuration, processName));
}
// Собирает LoggerConfiguration процесса: уровень/фильтры, rolling-файл, консоль.
// loggerConfiguration: Конфигурация Serilog (до CreateLogger).
// environment: Окружение хоста (Development — текстовая консоль).
// configuration: Конфигурация хоста (env DEAL_LOG_*).
// processName: Имя процесса (суффикс имени rolling-файла).
private static void Apply(
LoggerConfiguration loggerConfiguration,
IHostEnvironment environment,
IConfiguration configuration,
string processName)
{
loggerConfiguration
.MinimumLevel.Is(ParseMinimumLevel(configuration[MinimumLevelEnvKey]))
.MinimumLevel.Override(EntityFrameworkCoreCategory, LogEventLevel.Warning)
.MinimumLevel.Override(GrpcCategory, LogEventLevel.Information)
.Enrich.FromLogContext();
string logsDirectory = ResolveLogsDirectory(environment.ContentRootPath, configuration[LogsDirectoryEnvKey]);
Directory.CreateDirectory(logsDirectory);
string logFilePath = Path.Combine(
logsDirectory,
string.Format(LogFileNameTemplate, processName));
loggerConfiguration.WriteTo.File(
new CompactJsonFormatter(),
logFilePath,
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: RetainedFileCount);
if (environment.IsDevelopment())
{
// Dev: читаемый текст в консоли (Ruling 7: «dev можно текст»); файл — всегда JSON.
loggerConfiguration.WriteTo.Console(outputTemplate: DevelopmentConsoleTemplate);
}
else
{
// Prod-стиль: одна JSON-строка на событие — docker-логи собирает Promtail (Ruling 7/9).
loggerConfiguration.WriteTo.Console(new CompactJsonFormatter());
}
}
// Каталог rolling-файлов: env DEAL_LOGS_DIR либо data/logs под ContentRoot процесса.
// contentRootPath: ContentRoot хоста (/app в контейнере).
// configuredDirectory: Значение env DEAL_LOGS_DIR (null/пусто — дефолт).
// Возвращает: Абсолютный путь каталога логов.
private static string ResolveLogsDirectory(string contentRootPath, string? configuredDirectory)
=> string.IsNullOrWhiteSpace(configuredDirectory)
? Path.Combine(contentRootPath, DefaultLogsSubdirectory)
: configuredDirectory.Trim();
// Разбирает env DEAL_LOG_LEVEL; пустое/невалидное значение — DefaultMinimumLevel.
// rawValue: Сырое значение env.
// Возвращает: Уровень Serilog.
private static LogEventLevel ParseMinimumLevel(string? rawValue)
=> Enum.TryParse(rawValue, ignoreCase: true, out LogEventLevel parsedLevel)
? parsedLevel
: DefaultMinimumLevel;
}