9.7 KiB
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) seeAyCode.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:
- Writer management —
List<IAcLogWriterBase> LogWriters,GetWriters,Writer<T>() - Fan-out dispatch — Named methods (
Detail,Debug,Info,Warning,Suggest,Error) checkLogLevelthenForEachwriters - Write overloads — Terminal:
Write(AppType, LogLevel, text, caller, category, errorType, exMessage) - Write(IAcLogItemClient) — Accepts structured log items (from remote clients)
- ILogger bridge —
Log<TState>,IsEnabled,BeginScope(no-opNullScope) - [Conditional("DEBUG")] — Every named method has a
*Conditionalvariant 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 ofAppType(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
errorTypeis 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 |