AyCode.Core/AyCode.Core/docs/LOGGING/LOGGING_ISSUES.md

13 KiB

Logger — Known Issues

For planned/actionable work see LOGGING_TODO.md.

LOG-I-1: NopLogWriter ctor signature mismatch (consumer-specific but framework-exposed)

Severity: Minor (caught, non-blocking, but noisy) · Status: Open · Area: Writer-instantiation contract (AcLoggerBase(string) config-reading ctor)

Description

When AcLoggerBase(string? categoryName) (config-reading ctor) runs, it iterates AyCode:Logger:LogWriters[] and calls Activator.CreateInstance(writerType, AppType, LogLevel, categoryName). Some consumer writers (notably a NopCommerce-plugin-level NopLogWriter) have DI-injected ctors instead (e.g. (INopLoggerMsSqlNopDataProvider, CommonSettings, CustomerSettings, IWebHelper, ILogger, string?)), so no ctor matches → MissingMethodException.

Exception is caught in AcLoggerBase try/catch, logged to Console.Error, loop continues with other writers. Non-blocking — protocol construction succeeds — but produces noisy startup logs per Logger instance.

Root cause

Two logger-construction paradigms coexist:

  • Config-reading ctor expects (AppType, LogLevel, string?) writer ctors for Activator convenience
  • DI-injected writers have arbitrary ctor signatures

Known workaround

Console.Error noise tolerated. Alternatively, consumer uses DI-based AddAcLoggerFactory<TLogger> (see LOGGING.md) instead of the config-reading ctor — this path doesn't touch LogWriters[].

LOGGING_TODO.md#log-t-1

LOG-I-2: AcEnv.AppConfiguration is filesystem-bound, MAUI/WASM-unsafe

Severity: Minor · Status: Documented limitation · Area: AyCode.Core.Consts.AcEnv

Description

AcEnv.AppConfiguration is a static lazy singleton calling new ConfigurationBuilder().AddJsonFile("appsettings.json").Build() on first access. Reads current-working-directory filesystem. Throws on MAUI (no physical appsettings next to exe) and WASM (no filesystem at all).

Root cause

Design predates IOptions/DI pattern.

Known workaround

Consumer avoids the config-reading AcLoggerBase(string) ctor on these platforms. DI-based AddAcLoggerFactory<TLogger> + services.Configure<AcLoggerOptions>(...) is the replacement (see LOGGING.md).

LOGGING_TODO.md#log-t-2

LOG-I-3: Two parallel logger-setup patterns

Severity: Minor (confusion, not functional) · Status: Documented · Area: LOGGING.md / consumer code

Description

Two ways to construct a logger coexist:

  1. Config-reading: new Logger(categoryName)AcLoggerBase reads AyCode:Logger section via AcEnv.AppConfiguration, instantiates writers via Activator.CreateInstance
  2. DI factory: services.Configure<AcLoggerOptions>(...) + services.AddAcLoggerFactory<TLogger>()Func<string, TLogger> resolved from DI, writers pulled from DI

Consumer picks per scenario; no automatic bridge. Risk: mixing patterns causes subtle failures (e.g. MissingMethodException — see LOG-I-1).

LOGGING_TODO.md#log-t-3

LOG-I-4: Default LogLevel diverges across the two setup paths

Severity: Minor (surprise, not broken) · Status: Open · Area: AcLoggerBase field initializer vs AcLoggerOptions

Description

The two logger-setup paths (see LOG-I-3) ship with different default LogLevels:

  • AcLoggerBase.cs:18 — field initializer: LogLevel = Error
  • AcLoggerOptions.cs:27 — DI options default: LogLevel = Info

Consequences:

  • Config-reading ctor WITHOUT an AyCode:Logger:LogLevel entry → silent Error minimum (loses Info/Warning in prod)
  • DI factory WITHOUT services.Configure<AcLoggerOptions>(...)Info minimum

Same consumer code, same "no configuration" state, two different log volumes depending on which path was chosen.

Root cause

Historical: field initializer predates the Options class; Options was added as part of the DI-factory refactor with a developer-friendly Info default.

LOGGING_TODO.md#log-t-5

LOG-I-5: AcConsoleLogWriter.Initialize() runs twice on parameterless ctor

Severity: Minor (wasted work, not broken) · Status: Open · Area: AcConsoleLogWriter ctor chain

Description

AcConsoleLogWriter.cs:14-17:

protected AcConsoleLogWriter() : this(null)   // chains to (string?) ctor
{
    Initialize();                              // ← second call
}

protected AcConsoleLogWriter(string? categoryName = null) : base(categoryName)
{
    Initialize();                              // ← first call (via chain)
}

Chain : this(null) invokes the (string?) ctor, which calls Initialize(). Control returns to the parameterless ctor body and calls Initialize() again. Net effect: Console.ForegroundColor = ConsoleColor.White set twice — harmless but reveals a copy-paste oversight.

Fix direction

Remove the second Initialize() call in the parameterless ctor body (the chain already covered it).

LOGGING_TODO.md#log-t-6

LOG-I-6: ILogger.IsEnabled(MsLogLevel.None) incorrectly reports enabled

Severity: Low (semantic bug, rare path) · Status: Open · Area: AcLoggerBase.IsEnabled + MapFromMsLogLevel

Description

AcLoggerBase.cs:184-188:

public bool IsEnabled(MsLogLevel logLevel)
{
    var acLogLevel = MapFromMsLogLevel(logLevel);
    return LogLevel <= acLogLevel;
}

MapFromMsLogLevel(MsLogLevel.None) → AcLogLevel.Disabled (byte 255). For any instance LogLevel (max Error=25), LogLevel <= 255 is always trueIsEnabled(None) returns true despite None meaning "off".

The subsequent Log<TState> switch has case MsLogLevel.None: default: break; so no actual write occurs, BUT:

  • The MS-logging framework may query IsEnabled as a gate before calling Log — hosts using that pattern get misleading "enabled" signals
  • Any caller that inspects IsEnabled(None) (diagnostics, test assertions) gets the wrong answer

Fix direction

Either:

  • (a) Explicit if (logLevel == MsLogLevel.None) return false; at the top of IsEnabled
  • (b) Change the comparison to strict < with Disabled as sentinel (larger refactor — affects semantic of Disabled elsewhere)

LOGGING_TODO.md#log-t-7

LOG-I-7: Misleading inline comment in AcLoggerBase.Log<TState>

Severity: Trivial (doc-only) · Status: Open · Area: AcLoggerBase.cs:210-211

Description

// Use eventId.Name as memberName if available, otherwise null (will show as empty, not "Log")
var memberName = !string.IsNullOrEmpty(eventId.Name) ? $"{eventId.Name}:{eventId.Id}" : "Log";

Comment says fallback is null (empty display), the code assigns "Log". Contradicts both the doc in LOGGING.md#CallerMemberName Auto-Capture and the actual behaviour observed in logs.

Fix direction

Update comment to match code: // Use eventId.Name:eventId.Id if Name is set, otherwise fallback to "Log" per LOGGING.md convention.

Folded into LOGGING_TODO.md#log-t-8 (cleanup batch).

LOG-I-8: Server-side NopCommerce plugin still uses legacy config-reading Logger ctor

Severity: Minor (works, but inconsistent with modern pattern + triggers LOG-I-1 noise) · Status: Open · Area: Consumer adoption gap in Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs

Description

Client side (FruitBankHybridApp.* — Web, Web.Client, MAUI) was migrated to the DI-factory pattern: services.Configure<AcLoggerOptions>(...) + services.AddAcLoggerFactory<Logger>(). The server-side plugin was NOT migrated — it still:

  1. Directly constructs a logger via new Logger(nameof(AyCodeBinaryHubProtocol)) (PluginNopStartup.cs:169) and passes it as opts.Logger to the protocol — bypasses ILogger<AcBinaryHubProtocol> DI resolution that AcSignalRProtocolExtensions.BuildProtocol already implements.
  2. Does NOT call services.Configure<AcLoggerOptions>(configuration.GetSection("AyCode:Logger")).
  3. Does NOT call services.AddAcLoggerFactory<Logger>().
  4. Writer registration exists (services.AddScoped<IAcLogWriterBase, ConsoleLogWriter>() + NopLogWriter) but those DI-registered singletons are NOT the instances the new Logger(...) ctor sees — the legacy ctor creates a parallel set via Activator.CreateInstance.

The legacy config-reading ctor DOES find the appsettings AyCode:Logger section via AcEnv.AppConfiguration (filesystem-backed, works on server) — so logging functions. But every new Logger(...) call:

  • Triggers LOG-I-1 (NopLogWriter ctor mismatch → Console.Error noise)
  • Reconstructs writer instances via Activator (not singleton-shared with DI-registered writers)
  • Is inconsistent with the client-side pattern → two mental models for the same framework

Fix direction

See LOGGING_TODO.md#log-t-11.

  • LOG-I-1 (trigger — NopLogWriter ctor mismatch, currently causing Console.Error noise)
  • LOG-I-3 (root cause — two coexisting setup patterns)
  • LOG-I-4 (consequence — different defaults between paths; server legacy path silently ships with Error default unless AyCode:Logger:LogLevel is set)
  • Sibling gap: ../SIGNALR/SIGNALR_ISSUES.md#sig-i-7 (same server-side plugin, protocol-options adoption gap)
  • Plugin doc drift: Nop.Plugin.Misc.AIPlugin/docs/SIGNALR/README.md:22 still documents the pre-migration new AcBinaryHubProtocol() registration (actual code uses .AddAcBinaryProtocol(opts => {...})). Update needed.

LOG-I-9: JWT signing key written to log (CRITICAL security)

Severity: 🛑 Critical (security) · Status: Open · Area: AyCode.Services.Server/Logins/AcLoginServiceServer.cs:192

Description

GenerateAccessToken writes the JWT signing key to the logger via GlobalLogger.Detail:

GlobalLogger.Detail($"Key: {configuration["JWT:Key"]}");

The signing key is the cryptographic secret used to sign all JWT access tokens. Anyone with read access to the logs (file system, log aggregators, retention archives, operator dashboards, screenshots during incident response) can:

  1. Forge arbitrary tokens for any user
  2. Impersonate any role / claim
  3. Bypass authentication entirely

Log storage is typically at a lower trust level than dedicated secret stores. This exposure path is a complete authentication bypass.

Fix direction

Remove the GlobalLogger.Detail($"Key: ...") line entirely. The signing key must NEVER appear in any log output. If presence verification is needed for diagnostics, log only a hash prefix (e.g. Key configured: SHA256[0..8] = abc12345).

  • LOG-I-10 (sibling — access token also leaked, same file, same code path)
  • LOGGING/README.md may need a new "Never log secrets" guideline section (separate _TODO.md candidate)
  • Discovery context: this issue and LOG-I-10 emerged during the user-bearer-token ADR context-gathering phase (2026-04-25 Copilot session, while reading AcLoginServiceServer.cs). If a dedicated AUTH topic-folder is created later (likely as part of the bearer token ADR follow-ups), these two entries are candidates for relocation to AUTH_ISSUES.md with Status: SUPERSEDED by AUTH-I-N cross-references.

LOG-I-10: JWT access token written to log (CRITICAL security)

Severity: 🛑 Critical (security) · Status: Open · Area: AyCode.Services.Server/Logins/AcLoginServiceServer.cs:212

Description

After GenerateAccessToken produces a token, the token text itself is written to the logger:

GlobalLogger.Detail($"AccesToken: {writtenToken}");

(Side note: AccesToken is a typo for AccessToken — fix while at the line.)

Access tokens are bearer credentials — anyone holding the token can authenticate as the user until token expiry (currently 6h per GenerateAccessToken config). Logged tokens leak via the same channels as LOG-I-9 (live streams, retention archives, screenshots, shared logs during ops).

Fix direction

Remove the GlobalLogger.Detail($"AccesToken: ...") line entirely. If issuance verification is needed, log only metadata — user ID, expiry timestamp, issuer — never the token itself.

  • LOG-I-9 (sibling — JWT signing key also leaked, same file)
  • Same LOGGING/README.md "Never log secrets" guideline gap as LOG-I-9
  • Same discovery-context note as LOG-I-9 (bearer token ADR session, candidate for AUTH topic relocation if/when that emerges)

Evaluated review findings — NOT bugs (by-design)

Cross-session review (2026-04) flagged several items that are intentional defense-in-depth / YAGNI and should NOT be "fixed":

  • "Triple LogLevel gate on the ILogger.Log path is redundant" — NOT redundant. Info()/Debug() can be called directly (not only via ILogger.Log), so each named method MUST own its gate. Per-writer gate is the documented Gate-2 (see LOGGING.md "Two independent level gates"). Removing any gate breaks a real path.
  • "AcTextLogWriterBase.GetDiagnosticText defensive level-check is redundant" — same rationale; direct callers exist.
  • "IAcLoggerBase narrow interface (no dynamic LogLevel setter via interface)" — YAGNI. Consumers needing dynamic level can cast to AcLoggerBase. Widening the interface for a rare scenario pollutes the abstraction.
  • "protected List<IAcLogWriterBase> LogWriters is mutable" — acceptable; GetWriters returns a defensive copy ([.. LogWriters]), direct mutation is only reachable from derived classes (which control their own ctor-time population).