Skip to content

Plain Logback — ntfy-logback

Maven Central Javadoc

A Logback appender plus a zero-code auto-install via a Logback Configurator SPI. Because it installs through the logging framework itself, alerting is live at startup — before any DI container exists.

Use this when you use Logback (with or without Spring) and want ERROR logs to alert (and, with warnTopic, WARN logs too — see level-routing.md) — via XML, or with no config at all.

Runnable example: examples/logback — a plain SLF4J app wired through logback.xml, whose CI-run integration tests cover the base alert path, sync-vs-async delivery, and the clean-shutdown digest flush.

Install

<dependency>
  <groupId>io.github.pimak</groupId>
  <artifactId>ntfy-logback</artifactId>
  <version>2.1.0</version>
</dependency>

The appender pulls ntfy-core transitively — you only declare this one artifact.

Base configuration

Zero-code auto-install

Set config via the environment and the appender attaches itself to the root logger at startup (via a Logback Configurator SPI) — no logback.xml edit required:

export NTFY_URL=https://ntfy.example.com
export NTFY_TOPIC=my-app-alerts
export NTFY_TOKEN=tk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

Equivalently pass -Dntfy.url=… -Dntfy.topic=… as JVM system properties, or drop a classpath ntfy.properties with ntfy.url / ntfy.topic keys.

Classpath endpoints require an explicit opt-in. If the endpoint url comes only from a classpath ntfy.properties (no ntfy.url sysprop or NTFY_URL env var), the auto-install is refused with a warn status: any jar on the classpath can ship such a file, so honoring it blindly would let a compromised dependency redirect your error logs. If the file is yours, opt in with -Dntfy.allow-classpath-endpoint=true or NTFY_ALLOW_CLASSPATH_ENDPOINT=true (the flag itself is intentionally ignored inside ntfy.properties), or set the URL via env/sysprop instead.

Built-in async delivery

Delivery is asynchronous by default (since 2.0) — no AsyncAppender wrapper needed: each error alert is handed to the engine's bounded queue and published by a daemon worker, so a slow or unreachable ntfy server never blocks the logging thread. Overflow drops fold into the storm digest rather than being lost. See alert-behavior.md for the full semantics.

<appender name="NTFY_ALERT" class="io.github.pimak.ntfy.logback.LogbackAlertAppender">
  <url>https://ntfy.example.com</url>
  <topic>my-app-alerts</topic>
  <asyncQueueCapacity>1024</asyncQueueCapacity>
</appender>

Set <async>false</async> for pre-2.0 synchronous, inline delivery. The AsyncAppender wrapper below remains available if you prefer Logback's own offloading (pair it with <async>false</async> so delivery is not queued twice).

MDC context in alert bodies

Name the MDC keys you want rendered into each alert body and they appear as key: value lines just after the Logger: line, in the order listed:

<appender name="NTFY_ALERT" class="io.github.pimak.ntfy.logback.LogbackAlertAppender">
  <url>https://ntfy.example.com</url>
  <topic>my-app-alerts</topic>
  <includeMdcKeys>correlation-id,tenant</includeMdcKeys>
</appender>

This is an explicit allow-list with no wildcard: nothing from the MDC is published unless you name its key. Values are read from ILoggingEvent.getMDCPropertyMap(), which Logback captures when the event is constructed, so the context is correct even behind an AsyncAppender. Values are scrubbed and length-capped before rendering. See mdc-context.md for the guards and alert-behavior.md for where the block sits in the body.

Explicit logback.xml

Or wire it explicitly. The AsyncAppender wrapper is a production-grade alternative to the built-in async flag — async + never-block + ERROR-only + flush on shutdown:

<appender name="NTFY_ALERT_RAW" class="io.github.pimak.ntfy.logback.LogbackAlertAppender">
  <url>https://ntfy.example.com</url>
  <topic>my-app-alerts</topic>
  <token>tk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx</token>
  <async>false</async>
</appender>

<appender name="NTFY_ALERT" class="ch.qos.logback.classic.AsyncAppender">
  <appender-ref ref="NTFY_ALERT_RAW"/>
  <queueSize>256</queueSize>
  <neverBlock>true</neverBlock>
  <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
    <level>ERROR</level>
  </filter>
</appender>
<shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook"/>

<root level="INFO">
  <appender-ref ref="NTFY_ALERT"/>
</root>

Startup self-test

Alerting only speaks when something breaks, so a revoked token or a wrong topic stays invisible until the first real error alert fails to deliver. The opt-in self-test makes one round-trip at boot and reports a clear diagnostic instead:

<appender name="NTFY_ALERT" class="io.github.pimak.ntfy.logback.LogbackAlertAppender">
  <url>https://ntfy.example.com</url>
  <topic>my-app-alerts</topic>
  <startupPing>probe</startupPing>
</appender>

probe is read-only and publishes nothing; publish sends one low-priority test notification through the production path and is the only mode that proves alerts are genuinely deliverable (ntfy grants read and write separately). It runs in the background and never delays startup. Add <startupPingFailFast>true</startupPingFailFast> to turn a failure into a failed startup in CI or staging. Full details, including the failure diagnostics, in configuration.md.

startup-ping covers the primary topic only. The optional WARN route has its own independent flag — ntfy grants permissions per topic, so a healthy ERROR route proves nothing about the WARN one:

  <warnTopic>my-app-warnings</warnTopic>
  <startupPingWarn>probe</startupPingWarn>

When one route fails and another passed, the diagnosis is published as a notification on the passing route (startup-ping-notify-failures, on by default), so a broken route reaches you rather than only a status log.

The example, in full

The runnable examples/logback app is wired by exactly this file — CI compiles it and its integration tests run against it, so nothing shown here can drift from what is actually tested.

examples/logback/src/main/resources/logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
    Example Logback configuration wiring the ntfy alert appender explicitly. Every ntfy setting is
    resolved through Logback variable substitution with a default, so the same file works out of the
    box (NTFY_URL / NTFY_TOPIC environment variables, or -Dntfy… system properties) AND lets this
    module's integration tests point the appender at a loopback ntfy stand-in via context
    properties — the tests run exactly this file.

    Alternative: with ntfy-logback on the classpath you need no XML at all — setting NTFY_URL and
    NTFY_TOPIC auto-installs the appender through a Logback Configurator SPI (see docs/logback.md).
-->
<configuration>

  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <appender name="NTFY" class="io.github.pimak.ntfy.logback.LogbackAlertAppender">
    <url>${NTFY_URL:-https://ntfy.example.com}</url>
    <topic>${NTFY_TOPIC:-my-app-alerts}</topic>
    <!-- For an auth-protected topic: <token>tk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx</token> -->

    <!-- Built-in async delivery (the default since 2.0): each error alert is handed to a bounded
         queue drained by a daemon worker, so a slow or unreachable ntfy server never blocks the
         logging thread. Set false for pre-2.0 synchronous, inline delivery — the guarantee a
         short-lived process may prefer, since the publish completes before the logging call
         returns. -->
    <async>${NTFY_ASYNC:-true}</async>

    <!-- Storm suppression: at most maxAlertsPerWindow individual alerts per suppressionWindow
         (milliseconds); the overflow is reported as one aggregated digest — also flushed on a
         clean shutdown, so suppressed errors are never lost. These are the library defaults,
         spelled out so the example shows where to tune them. -->
    <maxAlertsPerWindow>${NTFY_MAX_ALERTS_PER_WINDOW:-3}</maxAlertsPerWindow>
    <suppressionWindow>${NTFY_SUPPRESSION_WINDOW:-180000}</suppressionWindow>
  </appender>

  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="NTFY"/>
  </root>

  <!-- Stops the LoggerContext (and therefore the ntfy appender) on JVM exit: the appender's stop()
       drains any queued async alerts and flushes the pending storm digest while the HTTP client is
       still live. -->
  <shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook"/>

</configuration>

Going further

The base config above covers the common case. Everything else is shared across all adapters and lives in the cross-cutting reference pages:

  • Configuration reference — every key and how it resolves (sysprop > env > ntfy.properties), the Logback XML setters, and duration syntax.
  • Authentication — token vs username/password and the token-wins rule.
  • Alert behavior — immediate alerts, storm suppression, and digests.
  • MDC context — the include-mdc-keys allow-list, its guards, and why there is no wildcard.
  • Level routing — the warn-topic opt-in: which levels alert and where.
  • Filtering — excluded-loggers, the NO_ALERT marker, and self-exclusion.
  • Troubleshooting — the diagnostics the engine emits via Logback's StatusManager, and how to surface them.
  • Compatibility — tested Logback / JDK versions.