Skip to content

Logging RU

Алексей . edited this page Jul 26, 2026 · 1 revision

Логирование

English Русский

LogsParser пишет диагностику через Microsoft.Extensions.Logging. На этой странице описаны LogsParserLogging — общий для всего процесса держатель фабрики, из которого каждый класс библиотеки получает свой логгер, — категории, попадающие в вывод, шкала уровней и способы подключить фабрику через DI, вручную или через Serilog.

LogsParserLogging

public static class LogsParserLogging

Пространство имён: LogsParser.Diagnostics.

Держатель хранит один ILoggerFactory и кеш ConcurrentDictionary<string, ILogger> с ключами по StringComparer.Ordinal. Пока фабрика не задана, держатель содержит NullLoggerFactory.Instance.

Член Сигнатура Описание
CreateLogger<T>() public static ILogger CreateLogger<T>() Возвращает закешированный логгер с категорией typeof(T).Name
CreateLogger(string) public static ILogger CreateLogger(string categoryName) Возвращает закешированный логгер для явно заданной категории
UseLoggerFactory(ILoggerFactory) public static void UseLoggerFactory(ILoggerFactory loggerFactory) Заменяет фабрику для всего процесса и очищает кеш логгеров
Reset() public static void Reset() Восстанавливает NullLoggerFactory.Instance и очищает кеш логгеров

CreateLogger<T>()

public static ILogger CreateLogger<T>()

Делегирует вызов в CreateLogger(typeof(T).Name), поэтому категорией становится короткое имя типа — LogsParserClient, а не LogsParser.LogsParserClient.

Параметр типа Описание
T Тип, короткое имя которого становится категорией лога
using LogsParser.Diagnostics;
using Microsoft.Extensions.Logging;

ILogger logger = LogsParserLogging.CreateLogger<MyImporter>();
logger.LogInformation("import started");

Исключения: не выбрасывает.

CreateLogger(string categoryName)

public static ILogger CreateLogger(string categoryName)

Достаёт логгер из кеша, создавая его через текущую фабрику при первом обращении. Повторные вызовы с той же категорией возвращают тот же экземпляр ILogger, пока фабрика не будет заменена или сброшена.

Параметр Тип По умолчанию Описание
categoryName string — Категория создаваемого логгера; используется как ключ кеша без изменений
using LogsParser.Diagnostics;
using Microsoft.Extensions.Logging;

ILogger logger = LogsParserLogging.CreateLogger("LogsHtmlParser");
logger.LogDebug("parsing a locally cached page");
Исключение Когда
ArgumentNullException categoryName равен null — он используется как ключ словаря

UseLoggerFactory(ILoggerFactory loggerFactory)

public static void UseLoggerFactory(ILoggerFactory loggerFactory)

Устанавливает loggerFactory в качестве фабрики для всего процесса и сбрасывает кеш логгеров, поэтому все логгеры, созданные после вызова, приходят уже из новой фабрики. Замена выполняется под внутренней блокировкой.

Параметр Тип По умолчанию Описание
loggerFactory ILoggerFactory — Фабрика, из которой будут создаваться логгеры при последующих вызовах CreateLogger
using LogsParser.Diagnostics;
using Microsoft.Extensions.Logging;

using var loggerFactory = LoggerFactory.Create(builder => builder
    .AddSimpleConsole()
    .SetMinimumLevel(LogLevel.Debug));

LogsParserLogging.UseLoggerFactory(loggerFactory);
Исключение Когда
ArgumentNullException loggerFactory равен null

Reset()

public static void Reset()

Возвращает NullLoggerFactory.Instance и очищает кеш. Метод нужен прежде всего для очистки в тестах: тест, установивший перехватывающую фабрику, обязан сбросить держатель после себя, иначе фабрика протечёт во все остальные тесты того же процесса.

using LogsParser.Diagnostics;

try
{
    LogsParserLogging.UseLoggerFactory(capturingFactory);
    // выполняем работу с библиотекой
}
finally
{
    LogsParserLogging.Reset();
}

Исключения: не выбрасывает.

Держатель фабрики общий для всего процесса

Фабрика одна на процесс, а не на клиент или на источник данных. Записывают в неё три действия:

Действие Эффект
LogsParserLogging.UseLoggerFactory(factory) Устанавливает factory глобально
new LogsParserHttpDataSource(..., loggerFactory: factory) Вызывает UseLoggerFactory(factory), если аргумент не null
services.AddLogsParser(...) + разрешение ILogsParserDataSource Зарегистрированная фабрика достаёт ILoggerFactory из контейнера (с откатом на NullLoggerFactory.Instance) и устанавливает его

Два следствия, которые стоит учитывать:

  • Создание второго LogsParserHttpDataSource с другим ILoggerFactory перенастраивает логирование для всего уже созданного, потому что каждый класс заново получает свой логгер при каждом обращении.
  • Разрешение источника данных из контейнера, в котором ILoggerFactory не зарегистрирован, установит NullLoggerFactory.Instance и тем самым молча заменит фабрику, установленную вручную раньше. Настраивайте логирование в том же контейнере либо вызывайте UseLoggerFactory после первого разрешения.
using LogsParser.Diagnostics;
using LogsParser.Models;
using LogsParser.Net;
using Microsoft.Extensions.Logging;

using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());

// Оба варианта ведут к одному и тому же глобальному держателю.
using var dataSource = new LogsParserHttpDataSource(
    credentials: new LogsParserCredentials("my_login", "my_password", "BASE32SECRET"),
    loggerFactory: loggerFactory);

LogsParserLogging.UseLoggerFactory(loggerFactory);

Логирование включается по желанию и ничего не стоит, пока не включено

Если фабрика не задана, любая категория разрешается в NullLogger. Его IsEnabled возвращает false, поэтому вызовы логирования внутри библиотеки не форматируют сообщения и не обращаются ни к одному приёмнику. Для работы библиотеки настройка не нужна, и до установки фабрики никуда ничего не пишется.

Категории

Категории — это короткие имена типов, а не полные имена с пространством имён. Вот те, что реально встречаются в выводе:

Категория Кто пишет О чём сообщает
LogsParserClient LogsParser.LogsParserClient Параметры вызовов и количество разобранных записей для каждого публичного метода
LogsParserHttpDataSource LogsParser.Net.LogsParserHttpDataSource Цикл «запрос — ответ», обработка редиректов, повторы, rate limit
LogsHtmlParser LogsParser.LogsHtmlParser Размеры HTML, количество записей, пропущенные строки, отсутствующая разметка
ReactShieldBypass внутренний решатель challenge Обнаружение challenge и результат его решения
CookieStorageExtensions внутренние помощники для cookies Имена cookies, добавленных в запрос, и cookies, добавленные или обновлённые из ответа
LogsParserAuthenticator внутренний auth flow Шаги входа и двухфакторного подтверждения, извлечение CSRF, отказы

ReactShieldBypass, CookieStorageExtensions и LogsParserAuthenticator — внутренние типы реализации: они не входят в публичный API, но их категории всё равно попадают в лог и фильтруются по имени.

Так как Microsoft.Extensions.Logging сопоставляет фильтры по префиксу категории, эти короткие имена работают напрямую и в AddFilter, и в appsettings.json:

using Microsoft.Extensions.Logging;

using var loggerFactory = LoggerFactory.Create(builder => builder
    .AddConsole()
    .SetMinimumLevel(LogLevel.Warning)
    .AddFilter("LogsParserHttpDataSource", LogLevel.Trace));

Шкала уровней

Уровень Область Примеры из исходного кода
Trace Детали обмена и cookies Sending GET {RelativeUri} (attempt {Attempt}/{MaxAttempts}); Response {RelativeUri}: StatusCode={StatusCode}, RateLimit={Remaining}/{Max}; Rate limit updated: {Remaining}/{Max}, Reset={Reset}; Cookies applied to request: {CookieCount} cookies [{CookieNames}]; Cookie {Action}: {CookieName}; CSRF token extracted from login page ({TokenLength} chars); TOTP code generated ({CodeLength} digits)
Debug Шаги потока выполнения и параметры вызовов LogsParserHttpDataSource initialized: BaseUri=…, HasCredentials=…, MaxRetryAttempts=…; GetLogsAsync: ServerId=…, Filters=[…], Period=…, Page=…, Limit=…; Step 1/4: GET /login — loading login page; ParseLogs: parsing HTML ({ContentLength} chars); Request completed: {RelativeUri}, {ContentLength} characters
Information Результаты и смена состояний GetLogsAsync: parsed {EntryCount} entries, MetaInfo={Start}-{End}/{Total}; Authentication required for {RelativeUri}, starting auth flow…; Two-factor confirmation completed for {Login}; React challenge detected for {RelativeUri}, solving…; React challenge solved via indexed array method
Warning Обратимая деградация Rate limit hit for {RelativeUri}, waiting until {ResetAt} ({DelaySeconds:F0}s); Transient failure for {RelativeUri}: retrying in {DelaySeconds}s (attempt {Attempt}/{MaxAttempts}); GetCurrentAccountAsync: account info not found in response; TOTP code rejected for {Login}: redirected back to /authenticator; Failed to parse IP address: '{RawValue}', using loopback fallback
Error Необратимые сбои Request failed: {RelativeUri} returned {StatusCode}; Account not configured: redirected to /profile for {RelativeUri}; Authentication did not stabilize for {RelativeUri} after {Attempts} attempts; Request failed after {MaxRetryAttempts} attempts: {RelativeUri}; React challenge unsolvable: tried {CandidateCount} candidates from {ValueCount} hex values

За каждой записью уровня Error следует выброшенное исключение — см. Исключения. Warning обычно означает обратимую ситуацию: цикл продолжает работу, выжидает окончание лимита, повторяет запрос или подставляет значение по умолчанию. Но не всегда: отклонённые учётные данные, отклонённый TOTP-код и отсутствующая разметка, из-за которой падают ParseAdminActivity и ParseTopOperations, тоже сначала записываются как Warning, а затем приводят к исключению.

Разумные отправные точки: Information для продакшена, Debug — когда запрос возвращает неожиданное количество записей, Trace — когда сбоит аутентификация или хранилище cookies.

Секреты не логируются

Библиотека пишет логин аккаунта и больше ничего, что позволяет опознать учётные данные:

Логируется Не логируется никогда
Login из LogsParserCredentials Пароль
Длина CSRF-токена ({TokenLength} chars) Значение CSRF-токена
Длина сгенерированного TOTP-кода ({CodeLength} digits) TOTP-секрет и любой сгенерированный код
Имена cookies и их количество Значение любой cookie, включая arizonarp_session, XSRF-TOKEN и R3ACTLB
Коды состояния ответов и длина содержимого Тела ответов

На уровне Trace вывод всё же показывает, какие страницы запрашивались и какие cookies существуют по именам, поэтому относитесь к trace-логам как к метаданным сессии, хотя секретов в них нет.

Подключение

Внедрение зависимостей

AddLogsParser подхватывает тот ILoggerFactory, который предоставляет контейнер, поэтому единственный дополнительный вызов — AddLogging.

using LogsParser;
using LogsParser.DependencyInjection;
using LogsParser.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var services = new ServiceCollection();

services.AddLogging(builder => builder
    .AddConsole()
    .SetMinimumLevel(LogLevel.Debug));

services.AddLogsParser(options =>
{
    options.Credentials = new LogsParserCredentials("my_login", "my_password", "BASE32SECRET");
});

await using var provider = services.BuildServiceProvider();

var client = provider.GetRequiredService<LogsParserClient>();
var page = await client.GetLogsAsync(new LogsQuery(ServerId: 201));

Console.WriteLine(page.Entries.Count);

Ручное подключение

Фабрику можно передать в источник данных либо установить напрямую; оба пути ведут к одному держателю.

using LogsParser;
using LogsParser.Abstractions;
using LogsParser.Models;
using LogsParser.Net;
using Microsoft.Extensions.Logging;

using var loggerFactory = LoggerFactory.Create(builder => builder
    .AddSimpleConsole(options => options.SingleLine = true)
    .SetMinimumLevel(LogLevel.Trace));

using var dataSource = new LogsParserHttpDataSource(
    credentials: new LogsParserCredentials("my_login", "my_password", "BASE32SECRET"),
    cookieStorage: new MemoryCookieStorage(),
    loggerFactory: loggerFactory);

var client = new LogsParserClient(dataSource);
var catalog = await client.GetLogsFilterCatalogAsync();

Console.WriteLine(catalog.Filters.Count);

У статических парсеров нет конструктора, куда можно передать фабрику, поэтому программа, использующая только Парсеры, устанавливает фабрику сама:

using LogsParser;
using LogsParser.Diagnostics;
using Microsoft.Extensions.Logging;

using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
LogsParserLogging.UseLoggerFactory(loggerFactory);

var page = LogsHtmlParser.ParseLogs(File.ReadAllText("saved-page.html"));

Serilog

Требуется пакет Serilog.Extensions.Logging, который адаптирует логгер Serilog к ILoggerFactory.

using LogsParser.Diagnostics;
using Microsoft.Extensions.Logging;
using Serilog;

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Console()
    .WriteTo.File("logs/logsparser-.log", rollingInterval: RollingInterval.Day)
    .CreateLogger();

using var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(Log.Logger, dispose: true));
LogsParserLogging.UseLoggerFactory(loggerFactory);

Собственная фильтрация Serilog работает по тем же коротким категориям, сопоставляемым через SourceContext:

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Warning()
    .MinimumLevel.Override("LogsParserHttpDataSource", Serilog.Events.LogEventLevel.Debug)
    .WriteTo.Console()
    .CreateLogger();

Минимальный уровень для отдельной категории

Приглушить парсеры, оставив подробный транспорт:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

services.AddLogging(builder => builder
    .AddConsole()
    .SetMinimumLevel(LogLevel.Information)
    .AddFilter("LogsParserHttpDataSource", LogLevel.Trace)
    .AddFilter("LogsParserAuthenticator", LogLevel.Debug)
    .AddFilter("CookieStorageExtensions", LogLevel.Warning)
    .AddFilter("LogsHtmlParser", LogLevel.Warning));

Та же конфигурация в appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "LogsParserHttpDataSource": "Trace",
      "LogsParserAuthenticator": "Debug",
      "CookieStorageExtensions": "Warning",
      "LogsHtmlParser": "Warning"
    }
  }
}

См. также

Clone this wiki locally