Integrations -> Abstractions/Exceptions/Extensions/Models/Options/ Services (включая Storage); Persistence-конфигурации -> Configurations; корень Deal.Api (оркестратор/планировщики/DTO) -> Services/Dtos. namespace приведён к путям, using добавлены/дедуплицированы, FQN обновлены.
127 lines
8.1 KiB
C#
127 lines
8.1 KiB
C#
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;
|
||
}
|