Skip to content

Spring Boot starter: @Scheduled and Quartz

The SilenceWatch Spring Boot starter monitors your application’s scheduled tasks without you declaring anything: it discovers every @Scheduled method and every Quartz job, declares them with their real schedule, and sends a heartbeat around every run. It is licensed under Apache 2.0, deliberately, so it can be embedded in any application.

pom.xml
<dependency>
<groupId>com.silencewatch</groupId>
<artifactId>silencewatch-spring-boot-starter</artifactId>
<version>0.1.0</version>
</dependency>
build.gradle
implementation 'com.silencewatch:silencewatch-spring-boot-starter:0.1.0'
application.yml
silencewatch:
api-key: ${SILENCEWATCH_API_KEY}

Create the API key in your project’s Settings › API keys. That is the whole setup.

On ApplicationReadyEvent, in one HTTP call:

  • Spring: it reads the tasks Spring itself scheduled, so the schedules are the resolved ones: @Scheduled(cron = "${backup.cron}") is declared with the expression the property actually held.
  • Quartz: if Quartz is present, it iterates the jobs and reads their triggers. A job with several triggers is declared from the one that fires most often, since that is the deadline a missed run breaches first.
  • It declares everything to POST /api/v1/checks/sync and keeps the returned ping keys in memory.

Each job gets a stable identity that ties it to its history across restarts, redeployments and renames: com.acme.jobs.BackupJob#run for Spring, group.jobName for Quartz. A job that disappears from the code is flagged orphaned, never deleted.

A Spring AOP advisor (plain Spring AOP, no AspectJ) wraps every @Scheduled method; a JobListener does the same for Quartz:

POST /p/<key>/start when the run begins
POST /p/<key>/0?duration_ms=1234 when it returns
POST /p/<key>/fail?duration_ms=1234 when it throws
silencewatch:
enabled: true # false removes everything, see below
api-key: ${SILENCEWATCH_API_KEY}
base-url: https://app.silencewatch.com # your own instance when self-hosting
environment: production # part of a check's identity
default-grace: 5m # for jobs that declare none
auto-register: true # false to manage checks by hand
report-start: true # false to send only the outcome
timeout: 2s # per call, deliberately short
queue-capacity: 1000 # bounded, oldest dropped when full
timezone: Europe/Paris # for cron jobs whose trigger carries none
discover-spring-tasks: true
discover-quartz-jobs: true

These are what make it safe to put in a production application:

  • It never fails your job. Every network or HTTP error is swallowed and logged at WARN. The interceptor calls your method exactly once, returns its result untouched, and rethrows its exception unchanged.
  • It never blocks your job. Sending a heartbeat is a queue offer handled by a low-priority daemon thread. An unreachable SilenceWatch costs your job nothing.
  • It is bounded. The queue holds at most queue-capacity heartbeats; past that the oldest are dropped, because the freshest state is the one worth reporting. Warnings are limited to one a minute.
  • It degrades silently. No API key, no network, no server: the application starts and runs exactly as it would without the dependency.
  • silencewatch.enabled: false removes the mechanism entirely: no discovery, no proxying, no threads, no beans.
  • No heavy dependencies. spring-boot-starter plus the JDK HTTP client. No JSON library, so no Jackson version is forced on your application.
  • Sub-30-second jobs are declared with a 30-second period, the shortest accepted, and a warning names them.
  • Quartz’s W cannot be evaluated by the server, so those jobs are skipped with a warning rather than failing the whole declaration.
  • Programmatically scheduled tasks (SchedulingConfigurer, lambdas) have no stable identity and are ignored. Declare those checks by hand, or give them a @Scheduled method.
  • spring.aop.auto=false disables the proxying Spring Boot normally provides: jobs are declared but no heartbeat can be sent. The starter says so at startup.