Class AlertEngine
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 Summary
FieldsModifier and TypeFieldDescriptionstatic final StringMarker name an adapter surfaces inAlertEvent.markerNames()to opt a single event out of alerting entirely. -
Constructor Summary
ConstructorsConstructorDescriptionAlertEngine(NtfyConfig config, Diagnostics diagnostics) AlertEngine(NtfyConfig config, Diagnostics diagnostics, PipelineCounters counters) Constructs an engine sharing an externally-ownedPipelineCounters. -
Method Summary
Modifier and TypeMethodDescriptioncounters()Read-only pipeline observability counters (published / suppressed / failed).booleanbooleanTrue when this started engine actually activated a WARN route, i.e.voidstart()url+topic both blank (or engine disabled) -> silently inactive (info,isStarted()==false).voidstop()Releases every resourcestart()acquired.voidsubmit(AlertEvent event) Assembles the alert payload, delegates toNtfyPublisher.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 injectedDiagnostics— never an SLF4J logger, never the token.
-
Field Details
-
NO_ALERT_MARKER_NAME
Marker name an adapter surfaces inAlertEvent.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
-
AlertEngine
Constructs an engine sharing an externally-ownedPipelineCounters. Adapters that outlive individual engines (e.g. the Logback appender acrossstart()/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() -
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 AFTERstart()rather thanNtfyConfig.isWarnRoutingEnabled():start()may withdraw the route that config asked for (an invalid warn topic, or one that reached the engine only through a classpathntfy.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
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 externalPipelineCounterswas 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 -> loudwarn, still inactive (partial config, likely a typo). Both token and username+password configured -> one-timewarn, but activation still proceeds (auth is optional and never blocks activation). Already started -> no-op, so a secondstart()without an interveningstop()can never overwrite and orphan the first executor/HttpClient. Active: build oneHttpClientplus its daemon-thread executor and construct theNtfyPublisher.synchronized(likestop()): two threads racingstart()— 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() -
submit
Assembles the alert payload, delegates toNtfyPublisher.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 injectedDiagnostics— 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 concurrentstop()that nulls the volatile fields simply causes a benign no-op return rather than an NPE misreported as an unexpected failure.
-