Filtering¶
The engine gives you four independent ways to control which log events actually generate a
notification. Three are configurable — excluded-loggers and excluded-exception-types on every
adapter, the NO_ALERT marker on the adapters whose framework has markers — and one is always on.
Looking for
include-mdc-keys? It is the opposite mechanism — it does not gate events at all, it selects which MDC context is attached to an event that already alerts — and it has its own page: mdc-context.md. Looking forwarn-topic? Level routing decides which levels alert and where, rather than filtering within a level, and it too has its own page: level-routing.md.Alerting is ERROR-only by default on every adapter. The core
AlertEnginedoes no level gating of its own beyond routing — it alerts on every event handed to it (subject to the gates below) — but each adapter gates before submitting:
- JUL (
ntfy-jul, standalone or installed by Quarkus) — the handler forwards onlySEVERE(ERROR-equivalent) records and above.- Logback (raw appender and the zero-code auto-install) — the appender submits only events at
ERRORor above. Without this floor, a root-logger auto-install would push every INFO/WARN line — request details, user identifiers, anything the app logs — verbatim to the ntfy topic. To alert on a subset of errors, scope the appender to specific loggers or useexcluded-loggers/theNO_ALERTmarker below.- Log4j2 (
<Ntfy>appender andNtfyLog4j2Installer) — the same floor, expressed asLevel.isMoreSpecificThan(Level.ERROR), so a<Root level="info">referencing the appender still alerts only on ERROR and above.The one way to widen this is
warn-topic, which adds a WARN route to its own topic with its own storm budget. It is an opt-in that must name its own destination, and it never touches the floor below it: INFO and under can never alert, on any configuration. That is not a validation rule but a property of the engine's level type, which has exactly two constants.
1. excluded-loggers — configurable logger-name exclusion¶
excluded-loggers (see configuration.md for where the key lives in each
framework) is a single, comma-separated string of logger-name prefixes. Any event whose logger name
equals one of the prefixes, or is a descendant of one (matched at a package-hierarchy boundary —
org.apache.kafka excludes org.apache.kafka.clients but not a sibling like
org.apache.kafkaconnect), is dropped before it is ever published or counted.
# Spring application.yml
ntfy:
url: https://ntfy.example.com
topic: my-app-alerts
excluded-loggers: org.apache.kafka, com.zaxxer.hikari
Excluded events disappear entirely: they are not published individually, and they are not counted toward the storm-suppression rate limiter or folded into a digest. Exclusion means "this is expected noise, never tell me about it" — not "tell me about it later, in bulk."
On startup, the engine announces its exclusion configuration once (alongside its ACTIVE
diagnostic), so you can confirm which prefixes are actually in effect. See
troubleshooting.md for the wording.
2. excluded-exception-types — configurable exception-type exclusion¶
Some failures are expected and already handled, and the exception type says so on its own: a client
that closed the connection mid-response, a cancelled request, a scheduled poll that timed out
against a service you already monitor elsewhere. excluded-exception-types names those types, and
an event whose exception chain contains any of them never alerts.
ntfy:
excluded-exception-types: org.apache.catalina.connector.ClientAbortException, java.util.concurrent.CancellationException
Two properties are worth being explicit about, because they are what make the key usable:
- The whole cause chain is matched, not just the surface throwable. A client disconnect almost never reaches a logger bare — it arrives wrapped in whatever the container, the framework or your own code threw around it. Matching only the outermost type would leave the key configured and inert, which is worse than not having it: the noise continues and the config says it should not. (The chain is capped at 64 links when the event is built, and stops at the first repeat, so a type buried below 64 wrappers is out of reach. No real stack goes there.)
- Names are matched whole, and must be fully qualified. Unlike
excluded-loggers, this is not prefix matching. A logger name is hierarchical, so a prefix names a subtree you can reason about; a class name is not, sojava.io.IOExceptionhere means that class and not everything whose name happens to start the same way.ClientAbortExceptionon its own matches nothing — write the package.
Use it when the type is what makes an error uninteresting. When it is the source that is noisy,
excluded-loggers is the better tool; when it is one specific call site, the NO_ALERT marker
below is.
3. The NO_ALERT marker — per-event opt-out (Logback and Log4j2)¶
Sometimes you want to suppress alerting for a single log statement rather than an entire logger. This
rides on the marker concept, so it is available on the Logback and Log4j2 adapters; the JUL
path — standalone or under Quarkus — has no markers at all and therefore no per-event opt-out.
LogbackAlertAppender exposes the constant NO_ALERT_MARKER_NAME (value "NO_ALERT") naming an
SLF4J marker you can attach to any individual error(...) call:
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import io.github.pimak.ntfy.logback.LogbackAlertAppender;
private static final Marker NO_ALERT =
MarkerFactory.getMarker(LogbackAlertAppender.NO_ALERT_MARKER_NAME);
log.error(NO_ALERT, "Expected, already-handled failure — do not page anyone");
An event carrying this marker (directly, or as a child of a composite marker referencing it) is gated out the same way an excluded-logger event is: never published, never counted, never folded into a digest. Use it for genuinely expected, already-handled error-level lines.
On Log4j2 the same marker name works with Log4j2's own MarkerManager markers, including a marker
that reaches NO_ALERT indirectly through its parents (Marker.getParents()):
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;
private static final Marker NO_ALERT = MarkerManager.getMarker("NO_ALERT");
log.error(NO_ALERT, "Expected, already-handled failure — do not page anyone");
4. Always-on self-exclusion — the anti-loop gate¶
The library's own package root, io.github.pimak.ntfy, is excluded from alerting
unconditionally — independent of excluded-loggers and independent of any framework-level filter.
A single root now covers every module in the family (core, jul, logback, log4j2, spring, quarkus).
This is a belt-and-suspenders guard against the engine ever generating a feedback loop by alerting on
a failure inside itself. (Diagnostics already go to a separate channel — Logback's StatusManager,
Log4j2's StatusLogger, or System.err for the JUL/Quarkus paths — never back through the logging
pipeline, so a loop could not form anyway; the self-exclusion holds even if that invariant is ever
violated by future code.)
You do not configure this; it cannot be turned off. It survives a blank or misconfigured
excluded-loggers value.
Because of this rule, an application whose own logger names start with
io.github.pimak.ntfywould be silently self-excluded. This effectively never happens for real apps, but it is why the integration-test app in this repo logs from acom.example.*package.
Why an exclude-list, not an allowlist, for loggers¶
excluded-loggers is deliberately an exclude-list (deny-list), not an allowlist. For an alerting
tool an allowlist has an inverted failure mode: an error from a logger you never thought to add would
be silently dropped — exactly the event you most need to be paged about. An exclude-list fails safe
instead: everything alerts by default, and you opt specific, already-understood noise sources out by
name. To scope alerting to a specific subtree, attach the appender (Logback, or an <AppenderRef> on
a non-root <Logger> in Log4j2) or handler (JUL, e.g.
NtfyJulInstaller.install(Logger.getLogger("com.example"))) to that logger rather than adding an
allowlist.
See also¶
- configuration.md — where
excluded-loggersandexcluded-exception-typeslive in each framework. - level-routing.md — the
warn-topicopt-in: which levels alert and where their alerts go. Everything on this page applies to both routes. - mdc-context.md — the
include-mdc-keysallow-list: attaching MDC context to the alerts that do get through. - alert-behavior.md — how surviving events are rate-limited and digested.