Skip to content

Audit Log

Pétrus Pradella edited this page Nov 1, 2026 · 3 revisions

Audit Log

"Who paid what, to whom, and when?" - a shop, a trade plugin or a quest plugin answers it with an AuditLog (package br.com.finalcraft.evernifecore.logic.audit): a persisted record of what that plugin moved, in its own storage, with retention and a query a command can page.

AuditLog log = AuditLog.of(getPluginData(), storage);                         // 30 days kept
AuditLog forever = AuditLog.of(getPluginData(), storage, AuditRetention.forever());

log.query(new AuditQuery().between(alex, bia).since(Instant.now().minus(Duration.ofDays(7))))
   .thenAccept(entries -> entries.forEach(entry -> sender.sendMessage(AuditCommandNode.line(entry))));

The in-memory AuditTrail of one run is a different thing (see Logic); the log is what survives.

What gets recorded

While open, the log listens on ECEventBus.global() and records only what its owner moved - the events whose Caller is that plugin:

Kind From
GATE_PAID, GATE_DENIED the plugin's gates
REFUND_FAILED a cost refund that failed
BALANCE_CHANGE a BalanceChangeEvent (the legs of a transfer are covered by the transfer)
TRANSFER a TransferEvent: the payer as getPlayer(), the receiver as getCounterparty()
DELIVERY_ENQUEUED, DELIVERY_CLAIMED, DELIVERY_EXPIRED, DELIVERY_PURGED, DELIVERY_DISCARDED the plugin's deliveries
BOOSTER_GRANTED its BoosterManager.grant (Boosters)
REWARD_PAID each member a Rewards.pay of the plugin paid, applied or delivered - the subject is the reward's owner (Groups)
DEPOSIT a deposit to one of its contributions (Dynamic Values)

record(entry) adds anything else; record(trail) files a run's AuditTrail (costs taken and refunded, refunds that failed, effects applied or failed - an effect that audited its own EFFECT_FAILED is not also EFFECT_APPLIED). Writing never waits: the entry is stored off the calling thread; awaitWrites() (for tests and shutdown) completes when every write started so far landed, a failed one too (it was logged).

AuditEntry

getId(), getKind(), getCaller() (plugin and origin), getOwner(), getInstant(), getText(), getAmount(), getPlayer(), getCounterparty() (the other party of a transfer, a trade or a give; null when none), getSubject() (the currency, item or variable id). Build one with AuditEntry.builder(kind, caller) - .player(uuid).counterparty(uuid).subject("money").amount(price).text("...").build().

AuditQuery

Each call narrows; none is required. Newest first, 50 at most unless limit says otherwise. Short-lived, not thread-safe: build it where it is used.

Call Keeps
player(uuid) / counterparty(uuid) entries of that player / with that other party
between(a, b) entries where a and b are player and counterparty, in either order
since(instant) entries from that instant on
kind(AuditKind) one kind
limit(n) at most n

Storage and retention

Collection ec_audit, indexed by at, player, kind and counterparty. AuditRetention.days(n) (30 by default) or AuditRetention.forever(); the old entries are swept once an hour (ECDebugModule.AUDIT logs how many). One log per plugin: AuditLog.ofIfPresent(owner), closed at the owner's shutdown (AuditLog.closeOwnedBy).

Showing it: AuditCommandNode

A plugin mounts the view on its own command:

@FinalCMD.Node(subcmd = "log", permission = "awshop.command.awshop.log")
private final AuditCommandNode log = new AuditCommandNode(auditLog, "awshop.command.awshop.log");

/awshop log [--player Steve] [--since 2h] [--kind transfer] [--page 2], or positionally /awshop log view [player] [since] [kind] [page], pages the entries, newest first, 20 a page: at | kind | player | counterparty | subject amount | caller origin. Every view needs <permissionBase>.view (awshop.command.awshop.log.view), checked before any argument is read.

For debugging, /ecore logic audit <plugin> [player] [since] [--page] (permission evernifecore.command.logic.audit) shows the same page for any plugin that has a log.

Testing

LogicWorld.auditLog() is the fake plugin's log over an in-memory storage; LogicWorld.awaitAudit() waits on its awaitWrites() before a query, instead of polling. A node is driven through the Commands harness like any command (see Testing Toolkit).

Clone this wiki locally