Class UltiPanelLogTransmitter

java.lang.Object
com.ultikits.ultitools.manager.UltiPanelLogTransmitter

@Internal public class UltiPanelLogTransmitter extends Object
UltiPanel log transmitter. Implements the log-transmission functionality per the API documentation specification.
Version:
1.0.0
Author:
UltiKits
  • Field Details

    • MAX_QUEUE_SIZE

      private static final int MAX_QUEUE_SIZE
      See Also:
    • MIN_INTERVAL_MS

      static final int MIN_INTERVAL_MS
      Shared lower bound for the batch-send interval (#432/WR-01). Before this fix, the boot-time path (LogStreamManager.loadBatchConfiguration()) clamped to 1000ms via its own inline Math.max(1000, interval), while this class's own setIntervalMs(int) -- reachable live, over the panel's WebSocket config action -- only rejected a non-positive value, leaving every value from 1ms up accepted. A panel operator (or a compromised/buggy panel session) could drive the batch scheduler to a roughly 1000-sends/second cadence indefinitely. Both entry points now share this one constant instead of the live path being silently more permissive than the boot path.
      See Also:
    • webSocketClient

      private final UltiPanelWebSocketClient webSocketClient
    • serverId

      private final String serverId
    • logTransmissionEnabled

      private final AtomicBoolean logTransmissionEnabled
    • batchModeLock

      private final Object batchModeLock
      Guards the batch-vs-immediate DISPATCH DECISION in sendLog(java.lang.String, java.lang.String, java.lang.String, java.lang.Throwable) against the enabled-to-disabled TRANSITION in setBatchEnabled(boolean) (Gate-2 finding, round 6). Before this lock, setBatchEnabled(false) wrote this.batchEnabled = false before flushLogs() drained the existing queue, so a concurrent sendLog() call could read the new false value and send a NEWER record immediately while flushLogs() was still draining OLDER queued records -- the newer record then arrived at the panel before the older ones, breaking FIFO order. Holding this lock across both sendLog's dispatch decision and the WHOLE disable transition means every sendLog call observes the transition as atomic: either entirely before it (queued, and included in the flush that is about to run) or entirely after it (flag already false, sent immediately) -- never during.
    • externalDrainMode

      private final AtomicBoolean externalDrainMode
    • externalSizeThresholdCallback

      private volatile Runnable externalSizeThresholdCallback
      Invoked from addToBatch(JsonObject) whenever the queue reaches batchSize WHILE externalDrainMode is active (Gate-2 finding, round 6). Without this, reaching the size threshold under external drain mode had no effect at all -- sendBatch()'s own early-return for external drain mode silently swallowed the threshold crossing that would otherwise have triggered an immediate send, so a documented size threshold never actually shortened delivery latency while monitoring was active, and a sustained burst could fill MAX_QUEUE_SIZE and start discarding old entries despite repeatedly crossing the threshold. null by default (no external owner wired up); LogStreamManager.initialize(com.ultikits.ultitools.websocket.UltiPanelWebSocketClient) sets it alongside setExternalDrainMode(boolean).
    • externalDrainCoordinationLock

      private volatile Object externalDrainCoordinationLock
      External coordination lock for flushLogs() (Gate-2 finding, round 9). null by default (no external owner wired up -- flushLogs() runs unsynchronized, its original behaviour). LogStreamManager.initialize(com.ultikits.ultitools.websocket.UltiPanelWebSocketClient) wires this to ServerMonitorManager's OWN logDrainLock (the same object ServerMonitorManager.drainAndSendLogsOnly(com.ultikits.ultitools.manager.UltiPanelLogTransmitter) and its inline sendBatchUpdate drain already synchronize on) alongside setExternalSizeThresholdCallback(java.lang.Runnable). Without this, a live batchConfig.enabled: false update's flush ran entirely outside that lock, so it could still send NEWER records ahead of OLDER ones the monitor had already polled but not yet sent, even after round 7's monitor-side-only serialization.
    • batchEnabled

      private volatile boolean batchEnabled
    • batchSize

      private volatile int batchSize
    • intervalMs

      private volatile int intervalMs
    • logQueue

      private final ConcurrentLinkedQueue<com.google.gson.JsonObject> logQueue
    • batchScheduler

      private final ScheduledExecutorService batchScheduler
    • batchSenderTask

      private volatile ScheduledFuture<?> batchSenderTask
      The currently-scheduled batch-send task, or null while batching is disabled.

      Tracked so setIntervalMs(int) and setBatchEnabled(boolean) can cancel and resubmit it -- before #432, the interval was baked into the one scheduleWithFixedDelay call the constructor made, and the setter only mutated the field without ever touching the already-running task.

  • Constructor Details

    • UltiPanelLogTransmitter

      public UltiPanelLogTransmitter(UltiPanelWebSocketClient webSocketClient, String serverId)
      Constructor.
      Parameters:
      webSocketClient - the WebSocket client
      serverId - the server ID
  • Method Details

    • sendLog

      public void sendLog(String level, String message, String source, Throwable throwable)
      Sends a log message to the backend.
      Parameters:
      level - the log level (info, warning, error, debug)
      message - the log message
      source - the log source (e.g.: "server", "plugin:name")
      throwable - the exception object (optional)
    • info

      public void info(String message, String source)
      Convenience methods: send a log at a specific level.
    • warning

      public void warning(String message, String source)
    • error

      public void error(String message, String source, Throwable throwable)
    • debug

      public void debug(String message, String source)
    • sendLogImmediately

      private void sendLogImmediately(com.google.gson.JsonObject logData)
      Sends a single log entry immediately.
    • addToBatch

      private void addToBatch(com.google.gson.JsonObject logData)
      Adds a log entry to the batch queue.
    • startBatchSender

      private void startBatchSender()
      (Re)starts the batch-send task at the current intervalMs. Cancels whatever task was previously scheduled first, so this is safe to call to both start fresh and reschedule (#432).
    • stopBatchSender

      private void stopBatchSender()
      Cancels the currently-scheduled batch-send task, if any, and clears the reference.
    • setIntervalMs

      public void setIntervalMs(int intervalMs)
      Sets the batch-send interval, in milliseconds, and -- unlike the field it replaces as a plain Lombok setter -- reschedules the already-running batch-send task to actually use it (#432). A no-op, deliberately not touching the scheduler, when the requested value equals the current one, so a live-config-update round-trip that resends the same value does not churn the scheduler. Has no effect on the scheduler while batching is disabled (there is no running task to reschedule); the new value still takes effect the next time batching is enabled.
      Parameters:
      intervalMs - the new interval; must be at least MIN_INTERVAL_MS
      Throws:
      IllegalArgumentException - if intervalMs is below MIN_INTERVAL_MS (including zero or negative) -- the previous interval is left in effect
    • setBatchSize

      public void setBatchSize(int batchSize)
      Sets the batch send-threshold size, rejecting anything below 1 (Gate-2 finding). A value of zero or negative would make sendBatch()'s own for (int i = 0; i < batchSize ...) loop consume nothing on every scheduled run, while addToBatch(JsonObject)'s logQueue.size() >= batchSize check is simultaneously always true (any non-negative queue size satisfies >= 0 or >= a negative number) -- so every single enqueued record would trigger an immediate sendBatch() call that dequeues nothing, silently stalling delivery while burning CPU on every log line, rather than the panel request being rejected outright. LogStreamManager#loadBatchConfiguration()'s own boot-time path already clamped to Math.max(1, batchSize); the live panel path had no lower bound at all before this fix, unlike setIntervalMs(int)'s pre-existing floor.
      Parameters:
      batchSize - the new batch size; must be at least 1
      Throws:
      IllegalArgumentException - if batchSize is below 1 -- the previous value is left in effect
    • setBatchEnabled

      public void setBatchEnabled(boolean batchEnabled)
      Enables or disables batched delivery, and -- unlike the field it replaces as a plain Lombok setter -- actually starts or stops the scheduled batch-send task to match (#432): disabling cancels it outright rather than leaving it running with nothing useful to flush; re-enabling starts a fresh one at the current intervalMs. A no-op when the requested value equals the current one.
      Parameters:
      batchEnabled - whether batched delivery should be active
    • sendBatch

      private void sendBatch()
      Sends the batched logs. When externalDrainMode is true, only drops entries that exceed the queue cap and does not send.

      Gate-2 finding (round 10): this method is ALSO reached directly, on the scheduler's own thread, by the periodically-firing batchSenderTask. Before this fix, that call was completely unsynchronized -- a live batchConfig.enabled: false update could hold batchModeLock across its own flushLogs() call while the scheduled task fired concurrently on the scheduler's thread and called this method too, both polling the SAME logQueue and both calling webSocketClient.sendMessage(...) independently -- interleaving queue polls between two frames with no ordering guarantee between the two sendMessage calls. Synchronizing the whole body on batchModeLock serializes the scheduled tick against setBatchEnabled(false)'s own flush, against flushLogs() however else it is reached (it now also acquires this same lock, see its own javadoc), and against sendLog's own dispatch decision (reentrant when this method is reached via addToBatch's size-threshold trigger, since that call chain already holds the lock). No new deadlock risk: this method never acquires any OTHER lock, so it can only ever be the innermost link in any lock chain that reaches it.

    • drainQueue

      public com.google.gson.JsonArray drainQueue(int maxItems)
      Drains log entries from the queue, returning them as a JsonArray. For external callers to use (e.g. ServerMonitorManager's batch_update).
      Parameters:
      maxItems - the maximum number of entries to take
      Returns:
      a JsonArray of log entries
    • setExternalDrainMode

      public void setExternalDrainMode(boolean enabled)
      Sets external drain mode. When enabled, sendBatch() no longer sends logs automatically; they are obtained externally via drainQueue() instead.
      Parameters:
      enabled - whether to enable external drain mode
    • isExternalDrainMode

      public boolean isExternalDrainMode()
      Checks whether external drain mode is active.
    • setExternalSizeThresholdCallback

      public void setExternalSizeThresholdCallback(Runnable callback)
      Sets the callback invoked when the queue reaches batchSize while external drain mode is active (Gate-2 finding, round 6). Pass null to clear it.
      Parameters:
      callback - a no-argument, non-blocking callback; called on whichever thread sendLog(String, String, String, Throwable) happened to run on
    • setExternalDrainCoordinationLock

      public void setExternalDrainCoordinationLock(Object lock)
      Sets the lock flushLogs() coordinates disable-time flushes against (Gate-2 finding, round 9). Pass null to clear it (flushLogs runs unsynchronized again).
      Parameters:
      lock - any object usable as a monitor; the SAME instance must be used by whatever external code also drains this transmitter's queue (see this field's own javadoc)
    • getStackTrace

      private String getStackTrace(Throwable throwable)
      Gets the exception stack trace as a string.
    • determineLoggerName

      private String determineLoggerName(String source)
      Determines the logger name.
    • getDefaultServerId

      private String getDefaultServerId()
      Gets the default server ID.
    • setLogTransmissionEnabled

      public void setLogTransmissionEnabled(boolean enabled)
      Enables/disables log transmission.
    • isLogTransmissionEnabled

      public boolean isLogTransmissionEnabled()
      Checks whether log transmission is enabled.
    • getQueueSize

      public int getQueueSize()
      Gets the current queue size.
    • flushLogs

      public void flushLogs()
      Immediately sends every log currently in the queue. Temporarily disables external drain mode to make sure the logs actually get sent.

      Gate-2 finding (round 10): now ALSO acquires batchModeLock as the OUTERMOST lock, before the external coordination lock below -- sendBatch() (which this method reaches via doFlushLogs()) is itself now synchronized on batchModeLock (see its own javadoc), so without this method also acquiring it first, a caller that had NOT already taken batchModeLock (e.g. shutdown(), or SystemLogHandler.flush()/close()) would acquire the coordination lock FIRST and only then try for batchModeLock inside sendBatch() -- the reverse of the order every other path in this class already establishes (batchModeLock before logDrainLock, see setExternalDrainCoordinationLock(Object)'s own javadoc), and a reverse-order acquisition on two different threads is exactly how a lock-ordering deadlock happens. Taking batchModeLock first here keeps every acquisition path in this class consistent with that one order, so this addition introduces no new deadlock risk -- verified by enumerating every lock-acquiring path in this class and ServerMonitorManager (see this plan's gate record).

    • doFlushLogs

      private void doFlushLogs()
    • shutdown

      public void shutdown()
      Shuts down the log transmitter.