# 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

```csharp
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`:

```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

```csharp
// 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).
//    The parameter is the HOST's default AppType (e.g. Web on a WASM/MAUI client) — applied only
//    when appsettings does NOT specify AyCode:Logger:AppType; a config value always wins.
services.AddAcLoggerFactory<MyConcreteLogger>(AppType.Web);
// Custom writer-marker overload — pulls only TWriterBase-registered writers:
// services.AddAcLoggerFactory<MyConcreteLogger, IAcLogWriterClientBase>(AppType.Web);
```

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

### Appsettings.json shape

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

Only `AppType` and `LogLevel` bind to `AcLoggerOptions`. **`AppType` resolution order (2026-07):** appsettings value (if present) → the `AddAcLoggerFactory(defaultAppType)` parameter → `Server` (the parameter's own default). `AcLoggerOptions.AppType` is nullable — `null` means "not configured", so the host's code default can apply; a config value always wins. `LogLevel` default: `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:
```csharp
public TLogger(AppType appType, LogLevel logLevel, string? categoryName, params IAcLogWriterBase[] logWriters)
```

If the concrete logger doesn't match, register a manual factory instead:
```csharp
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 management** — `List<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 bridge** — `Log<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`). **ResolveBridgeCallerNames** (default `true`): bridge calls without an `EventId` name show the real caller resolved from the stack trace (see CallerMemberName Auto-Capture).

### ILoggerProvider

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

```csharp
// 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 resolves the real caller from the stack trace (logging-infrastructure frames skipped, async state machines / lambdas unwrapped to the source method). Opt-out: `ResolveBridgeCallerNames = false` → plain `"Log"` fallback. The stack walk costs a few µs and only runs for enabled bridge logs without an `EventId` name; named AC methods never pay it.

## 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` |
