using Serilog; using Serilog.Events; using Serilog.Formatting.Compact; namespace Deal.Api.Logging; 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 к хосту /// /// Билдер 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()) { loggerConfiguration.WriteTo.Console(outputTemplate: DevelopmentConsoleTemplate); } else { 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; }