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
ErrorunderLogCategory.Module, with the handler's declaring type (and therefore its owning module, inferred from namespace) and the exception.Modulerather than the event's own category, deliberately: the bus has no reliable way to know which category an arbitraryIEvent"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
ErrorunderLogCategory.Persistencewith theDocumentKeyand 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.