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(), removed in 6.4.0) 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:
    • agentSink

      private final UltiPanelLogTransmitter.AgentLineSink agentSink
    • logTransmissionEnabled

      private final AtomicBoolean logTransmissionEnabled
    • batchModeLock

      private final Object batchModeLock
      Makes takePending() and adoptPending(JsonArray) atomic with respect to each other.
    • externalDrainMode

      private final AtomicBoolean externalDrainMode
    • batchEnabled

      private volatile boolean batchEnabled
    • batchSize

      private volatile int batchSize
    • intervalMs

      private volatile int intervalMs
    • logQueue

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

      private final AtomicReference<com.google.gson.JsonArray> heldBatch
      The one batch whose send failed, kept to be sent before anything newer (#486). Before 6.3.0 a batch was polled off logQueue and then sent, and nothing put it back when the send failed, so a socket that closed around the send lost the batch without a trace. While a batch is held, neither this class's own sender nor an external drain (drainQueue(int)) takes a newer record, so the panel still receives records in order. At most one batch is held, so a permanently failing send retries once per attempt and never spins, and the held records count against nothing else (the queue stays bounded by MAX_QUEUE_SIZE).

      A failed send is one that throws or after which the client no longer reports itself connected (the closed-socket path of UltiPanelWebSocketClient#sendMessage returns without sending). Whether the panel processed a frame that was sent cannot be known at this layer, so delivery stays best effort: a batch whose connection drops just after it was written may be sent again after reconnecting.

  • Constructor Details

  • 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)
    • buildLogData

      private com.google.gson.JsonObject buildLogData(String level, String message, String source, Throwable throwable, long timestamp)
      Builds one log entry as the panel receives it.

      This is the only place a cloud log entry is built, for the live stream (sendLog(java.lang.String, java.lang.String, java.lang.String, java.lang.Throwable)) and for the start-up replay (replayLog(java.lang.String, java.lang.String, java.lang.String, java.lang.Throwable, long)) alike, so the message and the stack trace are passed through SensitiveCommandRegistry.redactText(java.lang.String) here and no path can bypass it. The replay builds its entries when the stream opens, so a command a module registers while it enables also covers lines captured before it enabled. Only this copy is redacted: the LogRecord, the local console and the log files keep the original.

      Parameters:
      level - the log level, info when blank
      message - the message
      source - the log source, server when blank
      throwable - the exception, or null
      timestamp - the entry's time, in milliseconds since the epoch
      Returns:
      the entry
    • offerToAgent

      private void offerToAgent(String level, String message, String source, Throwable throwable, long timestamp)
      Hands one line to the v2 sink: the message and stack trace buildLogData(java.lang.String, java.lang.String, java.lang.String, java.lang.Throwable, long) redacted, the stack trace appended to the message because log.lines has no stack-trace field. Never throws: a log line must not break the logging call that produced it.
    • julLevel

      private static Level julLevel(String level)
      The JUL level of a panel level name, as SystemLogHandler#mapLogLevel produced it.
    • redact

      private static String redact(String text)
      Redacts registered commands' arguments from text bound for the panel. Without a framework instance nothing can have been registered, so the text is returned unchanged.
    • replayLog

      public void replayLog(String level, String message, String source, Throwable throwable, long timestamp)
      Queues one start-up record the early capture replays (#487) for delivery in batches.

      Queuing is an in-memory hand-off and does not depend on the connection: the capture has already given the record up, so a socket that closes while the capture is still handing its records over (#677) must not lose the rest. Nothing is sent here; startReplay()'s sender sends only while the client reports itself connected, and records still queued when the panel reconnects on a new client are handed to the next transmitter (takePending()). The transmission switch still applies -- nothing is queued while transmission is off, or when there is no client -- and the queue holds at most what EarlyLogCapture kept: at most 2,000 records and about 512 KiB, and nothing once it has waited five minutes for the stream.

      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)
      timestamp - when the record was logged -- kept, so the panel can place a replayed line among the live ones that may arrive while the replay is still being sent
    • startReplay

      public void startReplay()
      Does nothing as of 6.4.0: replayLog(java.lang.String, java.lang.String, java.lang.String, java.lang.Throwable, long) hands every replayed record to the sink at once, so no replay is ever queued. The protocol v1 replay sender this started was removed with the v1 client.
    • getReplayQueueSize

      public int getReplayQueueSize()
      How many replayed records are still waiting to be sent.
      Returns:
      the number of replayed records not yet delivered
    • 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)
    • 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 the former v1 sender's own for (int i = 0; i < batchSize ...) loop consume nothing on every scheduled run, while its 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
    • 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
    • holdUndelivered

      public void holdUndelivered(com.google.gson.JsonArray logs)
      Gives back a batch an external drain took with drainQueue(int) but could not send, so the next drainQueue(int) -- or this class's own sender -- sends it before any newer record (#486). ServerMonitorManager calls this when its batch_update frame carrying these records failed.
      Parameters:
      logs - the undelivered records, oldest first; ignored when null or empty
      Since:
      6.3.0
    • takePending

      public com.google.gson.JsonArray takePending()
      Removes and returns every record this transmitter has not sent -- replayed start-up records not yet delivered first, then the held batch, then the queue, oldest first -- so LogStreamManager can hand them to the transmitter that replaces this one when the panel reconnects on a new client (#486). Call it after shutdown(), whose flush sends what the old connection still can.
      Returns:
      the unsent records, oldest first; empty when there are none
      Since:
      6.3.0
    • adoptPending

      public void adoptPending(com.google.gson.JsonArray pending)
      Queues records a replaced transmitter could not send (takePending()) ahead of anything this one logs afterwards, so they reach the panel first and in order (#486). The queue's own bound applies: beyond MAX_QUEUE_SIZE the oldest are discarded and counted.
      Parameters:
      pending - the records, oldest first; ignored when null
      Since:
      6.3.0
    • 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.
    • 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()
      Does nothing as of 6.4.0: every line went to the sink when it was logged, and the protocol v1 sender that sent the queue was removed with the v1 client.
    • shutdown

      public void shutdown()
      Shuts down the log transmitter.