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; /// /// Подключает Serilog к хосту (builder.Host.UseSerilog). Регистрация отложенная: конфигурация /// логгера применяется при builder.Build(), когда среда/конфигурация (env) уже собраны. /// /// Билдер WebApplication процесса (до Build). /// Имя процесса для имени файла-лога (core/telegram/ai/ml). 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; }