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

9.7 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_SERVER.md. For remote writers (HTTP, browser console, SignalR) see AyCode.Services/docs/LOGGING_REMOTE.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.

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