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

12 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

Logger holds a list of writers. Every log call fans out, filtered by two independent level gates:

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 DB-synchronized with LogLevel table. Do NOT renumber.

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

Configuration

Config 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 ctor must accept (AppType, LogLevel, string?).

AcLogWriterBase also reads its own LogLevel from config by matching AssemblyQualifiedName against LogWriterType entries — a manually-instantiated writer (not via config) still picks up config values if a matching entry exists.

DI-Based Factory Pattern

Modern alternative to the config-reading AcLoggerBase(string) ctor. Concrete writer types resolved from DI; no runtime reflection over writer config. Recommended for 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
services.Configure<AcLoggerOptions>(configuration.GetSection("AyCode:Logger"));

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

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

Consumers inject Func<string, MyConcreteLogger> and invoke it with a category name.

Appsettings.json shape

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

Only AppType and LogLevel bind to AcLoggerOptions (defaults: Server / Info). Extend the POCO as needed; the binding picks up new properties automatically. The legacy LogWriters[] array (config-reading ctor) is unused in this pattern.

TLogger ctor requirement

AddAcLoggerFactory<TLogger> uses Activator.CreateInstance(typeof(TLogger), AppType, LogLevel, categoryName, writers)TLogger must expose:

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

If the concrete logger doesn't match, register a manual factory instead:

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

Companion extension: AddAcDefaults (SignalR)

For SignalR client setup, AcSignalRConnectionExtensions.AddAcDefaults(builder, logger, connectionOptions) bundles AddAcConnection + ConfigureLogging(AddAcLogger) in one 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 own LogLevel and AppType. Named methods (Detail, Debug, etc.) delegate to terminal Write(AppType, LogLevel, text, caller, category, errorType, exMessage).

Base Write throws NotImplementedException — subclasses must override either 7-param Write (text writers) or Write(IAcLogItemClient) (structured), 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

Log<TState> 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 → no-op NullScope (scopes not supported). ShortenCategoryNames (default true): when MS logging passes fully-qualified type names (e.g. Microsoft.AspNetCore.SignalR.HubConnectionHandler), shortens to class name only (HubConnectionHandler).

ILoggerProvider

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

// 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 (InfoConditional, ErrorConditional, ...) decorated [Conditional("DEBUG")] — stripped from Release builds. Provided by both AcLoggerBase and AcLogWriterBase. Use for dev-only diagnostics with zero release cost.

CallerMemberName Auto-Capture

Named log methods use [CallerMemberName] string? memberName = null — compiler auto-fills caller name. ILogger.Log<TState> (MS logging) uses EventId.Name if available, else "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