Class AlertEngine

java.lang.Object
io.github.pimak.ntfy.core.AlertEngine

public final class AlertEngine extends Object
Framework-pure alert engine: the extracted heart of the original Logback NtfyAlertAppender, decoupled from any logging framework. It is configured once via an immutable NtfyConfig, self-diagnoses exclusively through an injected Diagnostics sink (never an SLF4J logger, so no feedback loop is possible and no credential ever appears in diagnostic output), and is resource-clean across repeated start()/stop() cycles.

Adapters convert a framework event into an AlertEvent and call submit(io.github.pimak.ntfy.core.AlertEvent); the engine owns exclusion/marker gating, storm rate-limiting, payload assembly, HTTP publishing, and the periodic storm digest.

Delivery is asynchronous by default (since 2.0): individual error publishes are handed to a bounded work queue drained by a single daemon worker thread (ntfy-alert-delivery), so a slow or unreachable ntfy server can never back-pressure application threads during an error storm. When the queue is full an alert is dropped but folded into the storm digest's suppression count (never lost silently), and stop() drains any queued-but-unsent events into that same count before the final synchronous flush. Disabling async delivery restores inline synchronous publishing: HttpClient.send() then blocks the calling (logging) thread for up to connectTimeout + requestTimeout per event — the guarantee short-lived processes may prefer, since a queued alert that never reaches the worker before the JVM exits is not delivered.

  • Field Details

    • NO_ALERT_MARKER_NAME

      public static final String NO_ALERT_MARKER_NAME
      Marker name an adapter surfaces in AlertEvent.markerNames() to opt a single event out of alerting entirely. Public so callers have a stable handle instead of a magic string.
      See Also:
  • Constructor Details

    • AlertEngine

      public AlertEngine(NtfyConfig config, Diagnostics diagnostics)
    • AlertEngine

      public AlertEngine(NtfyConfig config, Diagnostics diagnostics, PipelineCounters counters)
      Constructs an engine sharing an externally-owned PipelineCounters. Adapters that outlive individual engines (e.g. the Logback appender across start()/stop() cycles and context resets) inject one long-lived holder here so the observability tallies stay monotonic across engine instances.
  • Method Details

    • isStarted

      public boolean isStarted()
      True once start() has activated the engine, until stop() tears it down.
    • isWarnRoutingActive

      public boolean isWarnRoutingActive()
      True when this started engine actually activated a WARN route, i.e. WARN events submitted to it will be published. Read this AFTER start() rather than NtfyConfig.isWarnRoutingEnabled(): start() may withdraw the route that config asked for (an invalid warn topic, or one that reached the engine only through a classpath ntfy.properties), and an adapter that gated on the request rather than the outcome would keep submitting warnings the engine silently drops — and, on Log4j2, would attach its appender a level too low. This is the single source of truth every adapter's WARN gate reads.
    • counters

      public PipelineCounters counters()
      Read-only pipeline observability counters (published / suppressed / failed). Callers only read; the increment methods are package-private so nothing outside the engine can mutate the tallies. The counters are pulled, never logged, and monotonic for this engine's lifetime — and, when an external PipelineCounters was injected, across engine instances sharing that holder.
    • start

      public void start()
      url+topic both blank (or engine disabled) -> silently inactive (info, isStarted()==false). Exactly one of url/topic blank -> loud warn, still inactive (partial config, likely a typo). Both token and username+password configured -> one-time warn, but activation still proceeds (auth is optional and never blocks activation). Already started -> no-op, so a second start() without an intervening stop() can never overwrite and orphan the first executor/HttpClient. Active: build one HttpClient plus its daemon-thread executor and construct the NtfyPublisher.

      synchronized (like stop()): two threads racing start() — e.g. a Logback reset listener vs. a Spring installer — would otherwise both pass the started check and both acquire an executor/HttpClient/digest scheduler, orphaning the loser's live threads and leaving its digest timer ticking forever. Cold path, so the lock costs nothing.

    • stop

      public void stop()
      Releases every resource start() acquired. Safe to call when never started (no NPE) and safe to call twice in a row — every field is guarded and nulled out. synchronized so it can never interleave with a concurrent start() and tear down a half-built engine.
    • submit

      public void submit(AlertEvent event)
      Assembles the alert payload, delegates to NtfyPublisher.publish(java.lang.String, java.lang.String, java.lang.String, io.github.pimak.ntfy.core.AuthMode, java.lang.String), and reports any failure exclusively through the injected Diagnostics — never an SLF4J logger, never the token. Exclusion and marker gates run BEFORE the publisher snapshot: gated events never publish and are never counted. A concurrent stop() that nulls the volatile fields simply causes a benign no-op return rather than an NPE misreported as an unexpected failure.