Skip to content

Logging and Diagnostics

How an operator finds out what happened. The original got this one right in spirit — "the original logged every meaningful event by category and could dump it to disk. Server operators need this." (01_VISION.md §2) — and this document keeps that property while giving it a typed, structured implementation.


1. The service

namespace Applejack.Core.Logging;

/// <summary>
/// Structured, categorised logging. Registered by CoreModule; every module resolves this
/// rather than calling a static logger, so log output is attributable and testable.
/// </summary>
public interface IGameLog
{
    void Info(LogCategory category, string message, params (string Key, object Value)[] data);
    void Warning(LogCategory category, string message, params (string Key, object Value)[] data);

    /// <summary>
    /// A failure worth an operator's attention with no exception object -- a validation
    /// rejection, a refused operation. See §3.
    /// </summary>
    void Error(LogCategory category, string message, params (string Key, object Value)[] data);

    /// <summary>
    /// A failure caused by a caught exception -- a failed persistence write, an event
    /// handler that threw. See §3.
    /// </summary>
    void Error(LogCategory category, string message, Exception exception, params (string Key, object Value)[] data);
}

Two overloads, not one method with Exception? exception = null ahead of params — a params parameter must be last, so an optional parameter before it can only be skipped by naming every later argument, and C# does not let a params array be supplied as a bare named tuple. Two overloads read naturally at every call site instead.

_log.Info(LogCategory.Economy, "door purchased",
    ("character", character.Id), ("door", door.Id), ("price", price));

2. Categories

A fixed, documented enum — the direct typed replacement for the legacy category taxonomy named as worth keeping in Legacy/00_OVERVIEW.md §6:

public enum LogCategory
{
    Core, Module,           // boot, dependency resolution, service registration
    Character, Economy, Inventory, Items, Ownership,
    Jobs, Needs, Crime, Law, Chat, Commands, World, Ui,
    Network, Persistence,
}

One category per module (00_OVERVIEW.md §1), plus Core and Module for framework-level events, Network and Persistence for the two cross-cutting concerns operators most often need to filter on when diagnosing a live issue.

3. What gets logged automatically

Two mechanisms elsewhere in this folder route through IGameLog without gameplay code doing anything extra:

  • Event handler exceptions (04_EVENT_BUS.md §4) — logged at Error under LogCategory.Module, with the handler's declaring type (and therefore its owning module, inferred from namespace) and the exception. Module rather than the event's own category, deliberately: the bus has no reliable way to know which category an arbitrary IEvent "belongs to" without every event type declaring one, which is machinery this project doesn't otherwise need — see Architecture/04_EVENT_BUS.md §4.
  • Persistence failures (06_PERSISTENCE.md §5) — a failed background save is logged at Error under LogCategory.Persistence with the DocumentKey and retry count.

Module and boot-lifecycle events (a module entering each stage, a dependency resolution failure) log automatically under LogCategory.Core / Module — see 02_MODULE_MODEL.md §2.

4. What must be logged deliberately

Gameplay code logs the events an operator would want in an audit trail: purchases, ownership changes, arrests, money transfers above a threshold, admin command usage, moderation actions. This is a review-time judgement call, not an enforced rule, but the standing question at review is: if this went wrong at 2am, would an operator need this line to find out why?

5. Sinks and export

IGameLog writes to the engine's standard log output by default. An operator-facing export — "dump categorised logs to disk", matching the original's operator workflow — is a Core service (ILogExporter, detailed alongside its implementation) that reads structured log entries and writes them to FileSystem.Data as newline-delimited JSON, filterable by category and time range. Because entries are structured ((string Key, object Value) pairs, not pre-formatted strings), this export needs no log-scraping or regex — it re-serialises what was already structured.

6. What this is not

IGameLog is diagnostics for operators and developers, not a gameplay feature. Player-facing notifications ("You don't have enough money") are a separate concern — 13_ERROR_HANDLING.md §3 — and never routed through the log, which would mix an audit trail with UI text and make both worse.

7. Testing

IGameLog is a plain interface; a fake implementation that records calls is used in tests asserting that a specific failure path logs at the expected level and category — for example, that the event bus's failure-isolation test (04_EVENT_BUS.md §6) also asserts an Error entry was recorded, not just that the second handler ran.