Skip to content

Logging

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

Logging

English Русский

LogsParser writes diagnostics through Microsoft.Extensions.Logging. This page documents LogsParserLogging — the process-wide factory holder every class in the library resolves its logger from — the category names that appear in the output, the level taxonomy, and how to wire a factory in DI, manually or through Serilog.

LogsParserLogging

public static class LogsParserLogging

Namespace: LogsParser.Diagnostics.

The holder keeps one ILoggerFactory and a ConcurrentDictionary<string, ILogger> cache keyed with StringComparer.Ordinal. Until a factory is supplied, the holder is NullLoggerFactory.Instance.

Member Signature Description
CreateLogger<T>() public static ILogger CreateLogger<T>() Returns a cached logger whose category is typeof(T).Name
CreateLogger(string) public static ILogger CreateLogger(string categoryName) Returns a cached logger for an explicit category name
UseLoggerFactory(ILoggerFactory) public static void UseLoggerFactory(ILoggerFactory loggerFactory) Replaces the process-wide factory and clears the logger cache
Reset() public static void Reset() Restores NullLoggerFactory.Instance and clears the logger cache

CreateLogger<T>()

public static ILogger CreateLogger<T>()

Delegates to CreateLogger(typeof(T).Name), so the category is the short type name — LogsParserClient, not LogsParser.LogsParserClient.

Type parameter Description
T The type whose short name becomes the log category
using LogsParser.Diagnostics;
using Microsoft.Extensions.Logging;

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

Throws: nothing.

CreateLogger(string categoryName)

public static ILogger CreateLogger(string categoryName)

Resolves the logger from the cache, creating it through the current factory on first use. Repeated calls with the same category return the same ILogger instance until the factory is replaced or reset.

Parameter Type Default Description
categoryName string — Category the logger is created with; used verbatim as the cache key
using LogsParser.Diagnostics;
using Microsoft.Extensions.Logging;

ILogger logger = LogsParserLogging.CreateLogger("LogsHtmlParser");
logger.LogDebug("parsing a locally cached page");
Throws When
ArgumentNullException categoryName is null — it is used as a dictionary key

UseLoggerFactory(ILoggerFactory loggerFactory)

public static void UseLoggerFactory(ILoggerFactory loggerFactory)

Installs loggerFactory as the factory for the whole process and throws away the logger cache, so loggers created afterwards come from the new factory. The swap is taken under an internal lock.

Parameter Type Default Description
loggerFactory ILoggerFactory — Factory every subsequent CreateLogger call resolves from
using LogsParser.Diagnostics;
using Microsoft.Extensions.Logging;

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

LogsParserLogging.UseLoggerFactory(loggerFactory);
Throws When
ArgumentNullException loggerFactory is null

Reset()

public static void Reset()

Restores NullLoggerFactory.Instance and clears the cache. It exists mainly for test cleanup: a test that installs a capturing factory must reset the holder afterwards, otherwise the factory leaks into every other test in the same process.

using LogsParser.Diagnostics;

try
{
    LogsParserLogging.UseLoggerFactory(capturingFactory);
    // exercise the library
}
finally
{
    LogsParserLogging.Reset();
}

Throws: nothing.

The factory holder is process-wide

There is one factory per process, not one per client or per data source. Three things write to it:

Action Effect
LogsParserLogging.UseLoggerFactory(factory) Installs factory globally
new LogsParserHttpDataSource(..., loggerFactory: factory) Calls UseLoggerFactory(factory) when the argument is not null
services.AddLogsParser(...) + resolving ILogsParserDataSource The registered factory resolves ILoggerFactory from the container (falling back to NullLoggerFactory.Instance) and installs it

Two consequences worth planning around:

  • Constructing a second LogsParserHttpDataSource with a different ILoggerFactory rewires logging for everything already constructed, because every class re-resolves its logger on each use.
  • Resolving the library's data source from a container that has no ILoggerFactory registered installs NullLoggerFactory.Instance, which silently replaces a factory that was installed manually beforehand. Configure logging in the same container, or call UseLoggerFactory after the first resolve.
using LogsParser.Diagnostics;
using LogsParser.Models;
using LogsParser.Net;
using Microsoft.Extensions.Logging;

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

// Both forms end up at the same global holder.
using var dataSource = new LogsParserHttpDataSource(
    credentials: new LogsParserCredentials("my_login", "my_password", "BASE32SECRET"),
    loggerFactory: loggerFactory);

LogsParserLogging.UseLoggerFactory(loggerFactory);

Logging is opt-in and free when unused

With no factory configured every category resolves to a NullLogger. Its IsEnabled returns false, so the library's log calls do not format messages and do not touch any sink. No configuration is required to use the library, and nothing is written anywhere until a factory is installed.

Categories

Categories are short type names, never fully-qualified names. These are the ones that appear in real output:

Category Emitted by What it reports
LogsParserClient LogsParser.LogsParserClient Call parameters and parsed result counts for every public method
LogsParserHttpDataSource LogsParser.Net.LogsParserHttpDataSource Request/response cycle, redirect handling, retries, rate limits
LogsHtmlParser LogsParser.LogsHtmlParser HTML sizes, entry counts, skipped rows, missing markup
ReactShieldBypass internal challenge solver Challenge detection and the outcome of solving it
CookieStorageExtensions internal cookie helpers Cookie names applied to a request and cookies added or updated from a response
LogsParserAuthenticator internal auth flow Login and two-factor steps, CSRF extraction, rejections

ReactShieldBypass, CookieStorageExtensions and LogsParserAuthenticator are internal implementation types — they are not part of the public API, but their categories still reach the log and can be filtered by name.

Because Microsoft.Extensions.Logging matches filters by category prefix, these short names work directly in AddFilter and in appsettings.json:

using Microsoft.Extensions.Logging;

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

Level taxonomy

Level Scope Examples from the source
Trace Wire and cookie detail 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 Flow steps and call parameters 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 Results and state transitions 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 Recoverable degradation 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 Terminal failures 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

Every Error entry is followed by a thrown exception — see Exceptions. A Warning is usually recoverable: the loop keeps going, waits out the rate limit, retries the request, or falls back to a default value. Some warnings still precede a throw — rejected credentials, a rejected TOTP code and the missing markup that makes ParseAdminActivity and ParseTopOperations fail are all logged at Warning first.

Useful starting points: Information for production, Debug when a query returns unexpected counts, Trace when the auth flow or the cookie jar misbehaves.

Secrets are never logged

The library logs the account login and nothing else that identifies a credential:

Logged Never logged
Login from LogsParserCredentials The password
CSRF token length ({TokenLength} chars) The CSRF token value
Generated TOTP code length ({CodeLength} digits) The TOTP secret, or any generated code
Cookie names and counts Any cookie value, including arizonarp_session, XSRF-TOKEN and R3ACTLB
Response status codes and content lengths Response bodies

At Trace the output still reveals which pages were requested and which cookies exist by name, so treat trace logs as session metadata even though they carry no secrets.

Wiring

Dependency injection

AddLogsParser picks up whatever ILoggerFactory the container provides, so AddLogging is the only extra call needed.

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);

Manual wiring

Either pass the factory to the data source or install it directly; both reach the same holder.

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);

The static parsers have no constructor to pass a factory to, so a program that only calls Parsers installs the factory itself:

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

Requires the Serilog.Extensions.Logging package, which adapts a Serilog logger to 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's own filtering works on the same short categories, matched through SourceContext:

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

Minimum level per category

Quiet the parsers, keep the transport verbose:

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));

The same configuration in appsettings.json:

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

See also

Clone this wiki locally