AyCode.Core/AyCode.Core/docs/LOGGING/README.md

14 KiB
Raw Blame History

Logging

Custom logging framework with multi-writer fan-out and Microsoft.Extensions.Logging integration. Source: Loggers/ in this project.

For server-side GlobalLogger see AyCode.Core.Server/docs/LOGGING/README.md. For remote writers (HTTP, browser console, SignalR) see AyCode.Services/docs/LOGGING/README.md.

Design Overview

A logger holds a list of writers. Every log call fans out to all writers that pass the level filter. Two independent level gates control what gets written:

logger.Info("msg")
  │
  ├─ Gate 1: Logger.LogLevel <= Info?         ← global minimum
  │    NO → discard
  │    YES ↓
  ├─ Writer[0].Write(...)
  │    └─ Gate 2: Writer.LogLevel <= Info?     ← per-writer minimum
  │         NO → discard
  │         YES → write to output
  ├─ Writer[1].Write(...)
  │    └─ Gate 2: ...
  └─ Writer[N].Write(...)
       └─ Gate 2: ...

Class Hierarchy

IAcLogWriterBase                              (writer contract)
  └─ AcLogWriterBase                          (abstract, config from appsettings)
       ├─ AcTextLogWriterBase                 (abstract, text formatting)
       │    ├─ AcConsoleLogWriter              (colored console output)
       │    └─ AcBrowserConsoleLogWriter       (AyCode.Services — Blazor JSInterop console)
       └─ AcLogItemWriterBase<TLogItem>       (AyCode.Entities — abstract, structured → ThreadPool + Mutex)
            ├─ AcDbLogItemWriter<TCtx,TItem>  (AyCode.Database — EF Core database writer)
            ├─ AcHttpClientLogItemWriter<T>   (AyCode.Services — HTTP POST as JSON)
            └─ AcSignaRClientLogItemWriter    (AyCode.Services — SignalR hub transport)

IAcLoggerBase (: IAcLogWriterBase, ILogger)
  └─ AcLoggerBase                             (abstract, multi-writer fan-out)
       └─ AcGlobalLoggerBase                  (sealed, used by GlobalLogger singleton)
       └─ [concrete loggers in consuming projects]

IAcLogWriterClientBase (: IAcLogWriterBase)   (marker for client-side writers)

Two writer branches:

Branch Base class Output Concurrency
Text AcTextLogWriterBase Formatted string → WriteText(string, LogLevel) Depends on subclass (Console uses lock)
Structured AcLogItemWriterBase<TLogItem> TLogItem object → WriteLogItemCallback(TLogItem) TaskHelper.RunOnThreadPool + Mutex

LogLevel

public enum LogLevel : byte
{
    Detail   = 0,
    Trace    = 5,
    Debug    = 10,
    Info     = 15,
    Suggest  = 17,
    Warning  = 20,
    Error    = 25,
    Disabled = 255,
}

⚠️ Values are synchronized with the database LogLevel table. Do NOT renumber.

Comparison is <=: a logger/writer with LogLevel = Info will process Info, Suggest, Warning, Error (anything ≥ 15).

Configuration

All config lives under AyCode:Logger in appsettings.json:

{
  "AyCode": {
    "Logger": {
      "AppType": "Server",
      "LogLevel": "Debug",
      "LogWriters": [
        {
          "LogWriterType": "MyApp.Loggers.MyConsoleLogWriter, MyApp",
          "LogLevel": "Info"
        },
        {
          "LogWriterType": "MyApp.Loggers.MyDbLogWriter, MyApp",
          "LogLevel": "Warning"
        }
      ]
    }
  }
}
Key Type Purpose
AppType AppType enum Identifies the application (Server, Web, etc.). Stamped on every log entry.
LogLevel LogLevel enum Global minimum — the logger's own gate.
LogWriters[] array One entry per writer. Each has LogWriterType (AssemblyQualifiedName) and LogLevel (per-writer gate).

Writer Instantiation

AcLoggerBase constructor iterates LogWriters[] and calls:

Activator.CreateInstance(logWriterType, AppType, logWriterLogLevel, CategoryName)

Each writer's constructor signature must accept (AppType, LogLevel, string?).

Writer Config Self-Lookup

AcLogWriterBase also reads its own LogLevel from config by matching its AssemblyQualifiedName against the LogWriterType entries. This means a writer instantiated manually (not via config) can still pick up config values if a matching entry exists.

DI-Based Factory Pattern

Modern, framework-first alternative to the config-reading AcLoggerBase(string) ctor. No runtime reflection over writer config; concrete writer types resolved from DI. Recommended for all modern projects (MAUI, WASM, ASP.NET Core) — the config-reading path is filesystem-bound and unsuitable for MAUI/WASM (see LOGGING_ISSUES.md#accore-log-i-r9p3).

Consumer setup in Program.cs

// 1. Bind options from appsettings.json
services.Configure<AcLoggerOptions>(configuration.GetSection("AyCode:Logger"));

// 2. Register writer(s) as DI singletons
services.AddSingleton<IAcLogWriterBase, MyConsoleWriter>();
// Or, for client-scoped marker:
// services.AddSingleton<IAcLogWriterClientBase, MyRemoteWriter>();

// 3. Register the logger factory — registers Func<string, TLogger> singleton in DI
services.AddAcLoggerFactory<MyConcreteLogger>();
// Or, with custom writer marker (pulls only TWriterBase-registered writers):
// services.AddAcLoggerFactory<MyConcreteLogger, IAcLogWriterClientBase>();

Consumers inject Func<string, MyConcreteLogger> and invoke it with a category name to obtain category-scoped logger instances.

Appsettings.json shape

{
  "AyCode": {
    "Logger": {
      "AppType": "Web",
      "LogLevel": "Debug"
    }
  }
}

Only AppType and LogLevel bind to AcLoggerOptions. The legacy LogWriters[] array used by the config-reading ctor is independent and unused in this pattern.

AcLoggerOptions

Property Type Default Purpose
AppType AppType Server Stamped on each log entry
LogLevel LogLevel Info Global minimum level (Logger's own gate)

Extend the POCO as needed; consumer's services.Configure<AcLoggerOptions> binding picks up new properties automatically.

TLogger ctor requirement

AddAcLoggerFactory<TLogger> uses Activator.CreateInstance(typeof(TLogger), AppType, LogLevel, categoryName, writers). TLogger must expose a public constructor with signature:

public TLogger(AppType appType, LogLevel logLevel, string? categoryName, params IAcLogWriterBase[] logWriters)

If your concrete logger doesn't have this signature, register a manual factory instead:

services.AddSingleton<Func<string, MyConcreteLogger>>(sp => category => new MyConcreteLogger(...));

Writer-marker scoping (two overloads)

  • AddAcLoggerFactory<TLogger>() — all IAcLogWriterBase-registered writers go into the logger
  • AddAcLoggerFactory<TLogger, TWriterBase>() — only writers registered as TWriterBase (e.g. IAcLogWriterClientBase for client-only)

Use the two-arg overload when the consumer separates client-only and server-only writers via a marker interface.

Companion extension: AddAcDefaults (SignalR)

For SignalR client setup that wires the same AcLoggerBase instance into Microsoft.Extensions.Logging, use AcSignalRConnectionExtensions.AddAcDefaults(builder, logger, connectionOptions) — it bundles AddAcConnection + ConfigureLogging(AddAcLogger) in a single call. See AyCode.Services/docs/SIGNALR/README.md.

Comparison: config-reading ctor vs DI-based factory

Aspect Config-reading ctor DI-based factory
Writer instantiation Activator.CreateInstance per writer entry Resolved from DI singletons
Writer config location AyCode:Logger:LogWriters[] services.AddSingleton<IAcLogWriterBase, ...>()
Writer ctor requirement (AppType, LogLevel, string?) Consumer-defined (via DI)
Writer lifecycle Per-logger instance Singleton, shared across loggers
Typical use Legacy / server-side config-driven Modern DI: MAUI, WASM, ASP.NET Core
MAUI/WASM-safe (filesystem-bound AcEnv.AppConfiguration)

Both patterns can coexist — consumer picks per scenario. Related issues: LOGGING_ISSUES.md. Planned unification: LOGGING_TODO.md#accore-log-t-r7l3.

Core Components

AcLoggerBase

Abstract logger implementing both IAcLogWriterBase and ILogger. Central responsibilities:

  1. Writer managementList<IAcLogWriterBase> LogWriters, GetWriters, Writer<T>()
  2. Fan-out dispatch — Named methods (Detail, Debug, Info, Warning, Suggest, Error) check LogLevel then ForEach writers
  3. Write overloads — Terminal: Write(AppType, LogLevel, text, caller, category, errorType, exMessage)
  4. Write(IAcLogItemClient) — Accepts structured log items (from remote clients)
  5. ILogger bridgeLog<TState>, IsEnabled, BeginScope (no-op NullScope)
  6. [Conditional("DEBUG")] — Every named method has a *Conditional variant stripped from Release builds

Constructors:

Signature Behavior
(string? categoryName) Reads config from appsettings.json, creates writers via Activator.CreateInstance
(string? categoryName, params IAcLogWriterBase[]) Reads AppType + LogLevel from config, uses provided writers
(AppType, LogLevel, string?, params IAcLogWriterBase[]) Fully manual, no config reading

AcLogWriterBase

Abstract writer base. Each writer has its own LogLevel and AppType. Named methods (Detail, Debug, etc.) delegate to the terminal Write(AppType, LogLevel, text, caller, category, errorType, exMessage) which subclasses override.

The base Write throws NotImplementedException — subclasses must override either the 7-parameter Write (for text writers) or Write(IAcLogItemClient) (for structured writers), or both.

AcTextLogWriterBase

Text formatting base for human-readable output. Overrides Write to format via GetDiagnosticText then calls abstract WriteText(string, LogLevel).

Output format:

[HH:mm:ss.fff] [S] [Level]   [Category->Method]                                      [ThreadId] Text
[ERROR_TYPE]: exception details...
  • Timestamp: DateTime.UtcNow.ToLocalTime() (local display, UTC storage)
  • [S] — First character of AppType (e.g., S=Server, W=Web)
  • Level: Left-padded to 9 chars
  • Category→Method: Left-padded to 54 chars
  • Error text on new line only if errorType is non-empty

AcConsoleLogWriter

Colored console output. Thread-safe via static readonly object ForWriterLock.

LogLevel Color
≤ Trace Gray
Debug Info White (default)
Suggest Cyan
Warning Yellow
≥ Error Red (with extra newlines)

Microsoft.Extensions.Logging Bridge

ILogger Bridge

AcLoggerBase implements ILogger directly. The Log<TState> method maps MS log levels to AC methods:

Microsoft.Extensions.Logging.LogLevel AyCode LogLevel Method called
Trace Detail Detail()
Debug Debug Debug()
Information Info Info()
Warning Warning Warning()
Error Error Error()
Critical Error Error("[CRITICAL] ...")
None Disabled — (ignored)

BeginScope returns a no-op NullScope (scopes not supported).

ShortenCategoryNames (default: true): When MS logging provides fully-qualified type names as categories (e.g., Microsoft.AspNetCore.SignalR.HubConnectionHandler), shortens to just the class name (HubConnectionHandler).

ILoggerProvider

AcLoggerProvider<TLogger> implements ILoggerProvider with a ConcurrentDictionary<string, TLogger> per-category cache. Factory function provided at registration.

Extension methods:

// Add alongside default providers
builder.Logging.AddAcLogger<MyLogger>(categoryName => new MyLogger(categoryName));

// Replace all providers with only AcLogger
builder.Logging.UseOnlyAcLogger<MyLogger>(categoryName => new MyLogger(categoryName));

Log Item Entity Hierarchy

Log items flow across projects as a chain of types:

IAcLogItemClient                       (AyCode.Core — DTO interface)
  └─ AcLogItemClient                   (AyCode.Entities — [MessagePackObject])
       └─ AcLogItem                    (AyCode.Entities.Server — [Table("LogItem")])

IAcLogItemClient Fields: TimeStampUtc, AppType, LogLevel, ThreadId, CategoryName, CallerName, Text, ErrorType, Exception.

Patterns

[Conditional("DEBUG")] Pattern

Every named log method has a *Conditional counterpart (e.g., InfoConditional, ErrorConditional) decorated with [Conditional("DEBUG")]. These are completely stripped from Release builds by the compiler. Both AcLoggerBase and AcLogWriterBase provide these. Use them for development-only diagnostics that should have zero cost in production.

CallerMemberName Auto-Capture

All named log methods use [CallerMemberName] string? memberName = null. The compiler auto-fills the calling method name. For ILogger.Log<TState> calls (from MS logging), the EventId.Name is used as member name if available, otherwise defaults to "Log".

Key Source Files

Component Path
Logger base Loggers/AcLoggerBase.cs
Writer base Loggers/AcLogWriterBase.cs
Text writer base Loggers/AcTextLogWriterBase.cs
Console writer Loggers/AcConsoleLogWriter.cs
ILoggerProvider Loggers/AcLoggerAdapter.cs
LogLevel enum Loggers/LogLevel.cs
Log item DTO interface Loggers/IAcLogItemClient.cs
Client marker Loggers/IAcLogWriterClientBase.cs