Class LogStreamManager

java.lang.Object
com.ultikits.ultitools.manager.LogStreamManager
All Implemented Interfaces:
org.bukkit.event.Listener

@Internal public class LogStreamManager extends Object implements org.bukkit.event.Listener
Log stream manager. Responsible for monitoring server logs in real time and transmitting them to UltiPanel via WebSocket.
Version:
2.0.0
Author:
UltiKits
  • Field Details

    • VALID_LOG_LEVELS

      private static final Set<String> VALID_LOG_LEVELS
      The only level names SystemLogHandler ever recognises -- exactly the vocabulary its own level mapping can produce ("info"/"warning"/"error"/"debug"), so a level a client asks to enable can always actually be reached by a published record's mapped level.
    • instance

      private static LogStreamManager instance
    • webSocketClient

      private UltiPanelWebSocketClient webSocketClient
    • gson

      private final com.google.gson.Gson gson
    • logTransmitter

      private UltiPanelLogTransmitter logTransmitter
    • systemLogHandler

      private SystemLogHandler systemLogHandler
  • Constructor Details

    • LogStreamManager

      private LogStreamManager()
  • Method Details

    • getInstance

      public static LogStreamManager getInstance()
    • initialize

      public void initialize(UltiPanelWebSocketClient client)
      Initializes the log stream manager.
    • loadBatchConfiguration

      private void loadBatchConfiguration()
      Loads the batch-send configuration from the config file.
    • sendInitializationLogs

      private void sendInitializationLogs()
      Sends the initialization-complete logs.
    • handleLogStreamMessage

      public void handleLogStreamMessage(com.google.gson.JsonObject data)
      Handles a log stream message.
    • handleConfigUpdate

      private void handleConfigUpdate(com.google.gson.JsonObject data, String clientId)
      Handles a configuration update.

      #433: a request naming an unrecognised level rejects the WHOLE request (previously-applied levels are left untouched) rather than partially applying the recognised entries -- a client that made a typo should not silently end up with a different filter than it asked for. An empty levels array is applied literally: it is a request for zero levels, so zero levels are enabled and nothing is delivered until the client sets levels again -- this framework's own front end never actually sends this field today (measured against both UltiPanelFrontend and ultipanel-api-worker while planning this fix), so there is no real product behaviour to match, and applying the request literally is the one reading that never has to guess what the caller "really" meant.

      Gate-2 finding: BOTH the levels section and the batchConfig section are parsed and validated FIRST, before either is applied. Before this fix, levels was applied immediately upon parsing; a request combining valid levels with an invalid batchConfig field left the levels change already in effect by the time the batchConfig validation failed and returned an error for the WHOLE request -- the same "declared rejection, partial effect" defect WR-02 already closed within batchConfig alone, reoccurring one level up, across the two top-level sections.

    • applyLevels

      private void applyLevels(LogStreamManager.LevelsSection levels, List<String> changes)
      Applies an already-validated levels section (if present) and appends a description to changes -- extracted from handleConfigUpdate(JsonObject, String) alongside applyBatchConfig(BatchConfigSection, List) (Codacy Gate-2 NPath complexity finding).
    • applyBatchConfig

      private void applyBatchConfig(LogStreamManager.BatchConfigSection batchConfig, List<String> changes)
      Applies an already-validated batchConfig section (if present) and appends a description to changes -- extracted from handleConfigUpdate(JsonObject, String) alongside applyLevels(LevelsSection, List) (Codacy Gate-2 NPath complexity finding).
    • parseAndValidateLevels

      private boolean parseAndValidateLevels(com.google.gson.JsonObject data, String clientId, LogStreamManager.LevelsSection out)
      Parses and validates the levels array from a config-update request into out, WITHOUT applying it (extracted from handleConfigUpdate(JsonObject, String), Codacy Gate-2 NPath complexity finding). out.present stays false if levels is absent from the request or systemLogHandler is not yet wired -- that is not a rejection.

      Rejects (sends the error response itself, matching this class's own established "validator sends its own rejection" convention) the WHOLE request if levels is present but not a JSON array (Gate-2 round 3), or if any entry is not one of VALID_LOG_LEVELS (#433) -- previously-applied levels survive either way.

      Returns:
      true if absent or valid; false if rejected -- the caller must return immediately without applying anything from either section
    • parseAndValidateBatchConfig

      private boolean parseAndValidateBatchConfig(com.google.gson.JsonObject data, String clientId, LogStreamManager.BatchConfigSection out)
      Parses and validates the batchConfig object from a config-update request into out, WITHOUT applying it (extracted from handleConfigUpdate(JsonObject, String), Codacy Gate-2 NPath complexity finding). out.present stays false if batchConfig is absent from the request or logTransmitter is not yet wired -- that is not a rejection.

      WR-01/WR-02: every present field is validated before any is applied -- a rejected size or interval must not leave the OTHER fields in this same batchConfig object already applied.

      Returns:
      true if absent or valid; false if rejected -- the caller must return immediately without applying anything from either section
    • parseAndValidateBatchEnabled

      private boolean parseAndValidateBatchEnabled(com.google.gson.JsonObject batchConfig, String clientId, LogStreamManager.BatchConfigSection out)
      Parses and validates batchConfig.enabled into out -- extracted from parseAndValidateBatchConfig(JsonObject, String, BatchConfigSection) alongside its two siblings (Codacy Gate-2 NPath complexity finding, round 7: the combined method peaked at 540 against a threshold of 200 once the round-6 strict-type checks were added inline).
    • parseAndValidateBatchSize

      private boolean parseAndValidateBatchSize(com.google.gson.JsonObject batchConfig, String clientId, LogStreamManager.BatchConfigSection out)
      Parses and validates batchConfig.size into out -- see parseAndValidateBatchEnabled(JsonObject, String, BatchConfigSection)'s javadoc for why this is a separate method.
    • parseAndValidateBatchInterval

      private boolean parseAndValidateBatchInterval(com.google.gson.JsonObject batchConfig, String clientId, LogStreamManager.BatchConfigSection out)
      Parses and validates batchConfig.interval into out -- see parseAndValidateBatchEnabled(JsonObject, String, BatchConfigSection)'s javadoc for why this is a separate method.
    • parseStrictInt

      private Integer parseStrictInt(com.google.gson.JsonElement element, String fieldName, String clientId)
      Parses element as a strict Java int -- rejects a non-numeric value, a non-integral number (e.g. 1.9), and a value outside int range (Gate-2 finding, round 6: Gson's getAsInt() silently truncates a fractional value and can wrap an out-of-range one, rather than validating that the payload is genuinely an integer). Sends the rejection response itself, matching this class's other validators.
      Returns:
      the parsed value, or null if invalid (rejection response already sent)
    • sendControlActionRejection

      private void sendControlActionRejection(String clientId, String action)
      Rejects a start/stop/pause/resume request (D-19/D-20, maintainer decisions 2026-09-14/2026-09-15).

      This framework has no per-viewer identity to gate any of these four actions with: the delivery messages (log_stream/log_batch) carry no per-client address at all -- UltiPanelLogTransmitter.sendLog(java.lang.String, java.lang.String, java.lang.String, java.lang.Throwable) broadcasts once over the single WebSocket connection this server holds to the panel relay, which fans a delivered record out to however many browser viewers are subscribed on that connection, invisibly to this framework. A server-side "real" implementation of any of the four would necessarily silence or resume delivery for every viewer of the server at once, not the one browser tab that asked.

      pause/resume (D-19): #434's original fix (a global "does any subscriber want delivery" check) could never actually suppress anything in production, because initialize(com.ultikits.ultitools.websocket.UltiPanelWebSocketClient) used to permanently subscribe a synthetic "auto" client that was never paused -- see 16-REVIEW-panel.md CR-01.

      start/stop (D-20, issue #468): measured end to end -- the shipped frontend sends start/stop only from its own toggle button and never gates rendering on the toggle state; the Worker's REST endpoint is a stateless relay minting a disposable clientId per call with no per-browser stream state anywhere in its Durable Object; and this framework's own (now-removed) stopLogStream mutated only its own internal bookkeeping, which nothing on the delivery path ever consulted. stop never actually stopped delivery for any real client, on any released version, before or after this change -- it changes only the response, not the underlying (already-inert) behaviour.

      The framework states plainly instead: it streams logs to the panel for as long as it is connected, with no per-viewer on/off state to toggle. Turning the live view on/off, or pausing/resuming it, is the panel view's own concern (stop/start rendering, optionally buffer client-side) -- not a request this server can selectively honour for one viewer. The removed startLogStream(String, String)/startLogStream(String)/ stopLogStream(String)/isStreaming()/getSubscriberCount() and (from D-19) pauseLogStream(String)/resumeLogStream(String) public methods are recorded in COMPATIBILITY.md under the same-release exception.

    • sendStreamResponse

      private void sendStreamResponse(String clientId, String status, String message)
      Sends a stream response message (used by the config action's acknowledgment).
    • sendErrorResponse

      private void sendErrorResponse(String clientId, String error)
      Sends an error response.
    • sendStreamStatus

      private void sendStreamStatus(String clientId)
      Sends the stream status (D-20, maintainer decision 2026-09-15).

      Unlike start/stop/pause/resume, status asks nothing to be toggled -- it is a read-only introspection request, and every field it reports is backed by something genuinely tracked, not bookkeeping that no client's request ever actually drove. There is no longer a subscriberCount or streaming field: this framework has no concept of an individual subscribed viewer to count (see sendControlActionRejection(java.lang.String, java.lang.String)'s javadoc), and delivery is unconditional once the handler is attached, for as long as the panel connection is up -- which is exactly what the new connected field reports honestly. logTransmitterEnabled/queueSize are unchanged; both were already backed by real UltiPanelLogTransmitter state.

    • sendCustomLog

      public void sendCustomLog(String level, String message, String source)
      Sends a custom log message directly. Used for logging plugin-specific events.
    • sendPlayerEventLog

      public void sendPlayerEventLog(String eventType, String playerName, String message)
      Sends a player-event log.
    • sendPluginActionLog

      public void sendPluginActionLog(String action, String details)
      Sends a plugin-action log.
    • onPlayerJoin

      public void onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent event)
    • onPlayerQuit

      public void onPlayerQuit(org.bukkit.event.player.PlayerQuitEvent event)
    • onServerLoad

      public void onServerLoad(org.bukkit.event.server.ServerLoadEvent event)
    • getServerId

      private String getServerId()
      Gets the server ID.
    • shutdown

      public void shutdown()
      Shuts down the log stream manager.
    • detachAllSystemLogHandlers

      private void detachAllSystemLogHandlers()
      Detaches every one of this framework's SystemLogHandler instances from the JVM root logger.

      Deliberately scans by type and removes every instance, rather than only removing this.systemLogHandler: before this method existed, every successful reconnect attached one more to the root logger, the field could only ever hold onto the last one, and the previously-leaked ones had no reference pointing at them any more and so could never be detached. Only a type-based scan can clean up the whole historical backlog at once, and that is also the only formulation under which the acceptance criterion "handler count is always 1" can hold. See issue #181.