Class ErrorReportCollector
Three-layer protection against DB bloat: 1. Fingerprint dedup within 5-minute window 2. Max 10 unique errors per batch cycle 3. Sampling after 10 total occurrences of same error
This class NEVER uses java.util.logging.Logger to prevent circular logging. All internal error output uses System.err.
On Paper, System.err does not bypass the logger: SysoutCatcher re-logs a plugin's
System.err line through that plugin's logger, so it can reach SystemLogHandler (#584).
That is safe here because of where the two lines run: reportError(java.lang.Throwable, java.lang.String, com.ultikits.ultitools.manager.TriggerContext) is called from
SystemLogHandler (inside its same-thread re-entry guard, which drops the re-logged copy),
from command execution, the cooldown validator, the declarative GUI scheduler and the AOP exception
interceptor, and the configuration is loaded at start-up and reload. Only the first can run on a
thread holding a lock of the log stream, and there the re-entry guard drops the copy before the
transmitter is reached (and UltiPanelLogTransmitter never takes its batch lock on a thread
holding the drain lock); from every other caller the line at worst reaches the stream as an
ordinary line.
- Since:
- 6.2.3
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescription(package private) static classError report data structure. -
Field Summary
FieldsModifier and TypeFieldDescriptionprivate static final longprivate final ConcurrentHashMap<String, ErrorReportCollector.ErrorReport> private intprivate booleanprivate final ConcurrentLinkedQueue<ErrorReportCollector.ErrorReport> private static final intprivate final ConcurrentHashMap<String, AtomicInteger> private static final intprivate static final intprivate static final intprivate static final intprivate intprivate intprivate doubleprivate ScheduledExecutorService -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionprivate voidCapture current server state (TPS, memory, player count).private static StringcomputeFingerprint(Throwable throwable) Compute a fingerprint for dedup: hash of (exception class + top N stack frame method:line).private booleancomputeIsFramework(Throwable throwable, String moduleName) Determine whether an error originates from the UltiTools framework itself (as opposed to a plugin module loaded by the framework).private ErrorReportCollector.ErrorReportcreateReport(Throwable throwable, String fingerprint, String moduleName, TriggerContext ctx) Create an ErrorReport from a throwable.(package private) List<com.google.gson.JsonObject> drainErrorReportPayloads(int max) Drains the pending reports as v2error.reportpayloads (plan 22-22, S-55), with the same limit, merge and order asdrainErrors(int).com.google.gson.JsonArraydrainErrors(int max) Drain errors into a JsonArray for batch_update.private List<ErrorReportCollector.ErrorReport> drainReports(int max) (package private) static StringfingerprintOf(Throwable throwable) private StringformatStackTrace(Throwable throwable) Format stack trace to string, truncated to MAX_STACK_TRACE_LENGTH.voidinit()Initialize the collector and start the dedup window reset scheduler.booleanCheck if error reporting is enabled.voidLoad configuration from UltiTools config.yml.(package private) static StringmoduleIdentifierOf(String moduleName) The UltiCloud identifier of the loaded module a report names (22-26 fix round, backend contract answer of 2026-10-27): the server groups anerror.reportto a module by matching itsmoduleNameagainst the catalogue identifier, so the v2 report carries the module's identify-string, lower-case (ModuleIds), never free text.private static StringRedacts registered commands' arguments from a field bound for UltiCloud.voidreportError(Throwable throwable, String moduleName, TriggerContext ctx) Report an error.private voidReset the dedup window — allows the same error to be reported again.voidshutdown()Shutdown the collector.
-
Field Details
-
MAX_QUEUE_SIZE
private static final int MAX_QUEUE_SIZE- See Also:
-
MAX_MESSAGE_LENGTH
private static final int MAX_MESSAGE_LENGTH- See Also:
-
MAX_STACK_TRACE_LENGTH
private static final int MAX_STACK_TRACE_LENGTH- See Also:
-
MAX_STACK_FRAMES
private static final int MAX_STACK_FRAMES- See Also:
-
FINGERPRINT_STACK_FRAMES
private static final int FINGERPRINT_STACK_FRAMES- See Also:
-
BYTES_PER_MB
private static final long BYTES_PER_MB- See Also:
-
enabled
private volatile boolean enabled -
dedupWindowSeconds
private volatile int dedupWindowSeconds -
maxErrorsPerBatch
private volatile int maxErrorsPerBatch -
sampleAfterCount
private volatile int sampleAfterCount -
sampleRate
private volatile double sampleRate -
dedupMap
-
errorQueue
-
lifetimeCounters
-
scheduler
-
-
Constructor Details
-
ErrorReportCollector
public ErrorReportCollector()
-
-
Method Details
-
init
public void init()Initialize the collector and start the dedup window reset scheduler. -
shutdown
public void shutdown()Shutdown the collector. -
loadConfiguration
public void loadConfiguration()Load configuration from UltiTools config.yml. -
reportError
Report an error. Thread-safe. Returns immediately if disabled.- Parameters:
throwable- the exceptionmoduleName- which UltiTools module caused it (e.g., "UltiChat")ctx- trigger context (who/what caused this)
-
drainErrors
public com.google.gson.JsonArray drainErrors(int max) Drain errors into a JsonArray for batch_update. Layer 2: returns at most maxErrorsPerBatch, sorted by count descending. Performs a final dedup pass to merge entries with the same fingerprint.- Parameters:
max- maximum number of errors to drain- Returns:
- JsonArray of error reports
-
drainErrorReportPayloads
Drains the pending reports as v2error.reportpayloads (plan 22-22, S-55), with the same limit, merge and order asdrainErrors(int). Each payload is built byAgentEventEncoders.errorReport(com.ultikits.ultitools.manager.AgentEventEncoders.ErrorReportView)fromErrorReportCollector.ErrorReport.view(), so the message, stack trace and trigger detail are redacted exactly as the v1 report is, and the fingerprint is this collector's group key padded to eight hex digits. A report the encoder cannot make valid is dropped. A repeat of a report already drained inside the dedupe window is counted on it and not sent again, as before.- Parameters:
max- the most payloads to return- Returns:
- the payloads, most frequent first; empty while error reporting is off
-
drainReports
-
resetDedupWindow
private void resetDedupWindow()Reset the dedup window — allows the same error to be reported again. -
fingerprintOf
- Parameters:
throwable- an exception- Returns:
- the group key this collector files it under; the panel's error group id is this key padded to eight hex digits (S-55)
-
computeFingerprint
Compute a fingerprint for dedup: hash of (exception class + top N stack frame method:line). -
createReport
private ErrorReportCollector.ErrorReport createReport(Throwable throwable, String fingerprint, String moduleName, TriggerContext ctx) Create an ErrorReport from a throwable. -
captureServerState
Capture current server state (TPS, memory, player count). -
formatStackTrace
Format stack trace to string, truncated to MAX_STACK_TRACE_LENGTH. -
computeIsFramework
Determine whether an error originates from the UltiTools framework itself (as opposed to a plugin module loaded by the framework).A framework error satisfies BOTH: 1. The top stack frame's class is in the com.ultikits.ultitools package. 2. The module name indicates the framework (null, empty, or "UltiTools").
-
moduleIdentifierOf
The UltiCloud identifier of the loaded module a report names (22-26 fix round, backend contract answer of 2026-10-27): the server groups anerror.reportto a module by matching itsmoduleNameagainst the catalogue identifier, so the v2 report carries the module's identify-string, lower-case (ModuleIds), never free text. A name is attributed to a loaded module when it equals that module's name or identify-string, ignoring case; anything else -- the framework itself, a Bukkit plugin, the server, a command description, a bean name -- givesnull, which the contract allows. Never throws.- Parameters:
moduleName- the name a producer passed, possiblynull- Returns:
- the identifier, or
nullwhen the report is not attributable to a loaded module
-
redact
Redacts registered commands' arguments from a field bound for UltiCloud. Without a framework instance nothing can have been registered, so the text is returned unchanged. -
isEnabled
public boolean isEnabled()Check if error reporting is enabled.- Returns:
- true if enabled
-