Docs › Spring @Scheduled monitoring with RunVouch
Spring @Scheduled monitoring with RunVouch
Spring @Scheduled monitoring for the log line nobody grepped: @Scheduled(cron = "0 0 2 * * *") runs inside the application. An exception is caught by the scheduler's error handler, logged at ERROR, and the next fire proceeds as planned. A method that hangs on a socket blocks the default one-thread scheduler and every other task with it.
How it runs on Spring @Scheduled
At the top and bottom of the scheduled method with Java's built-in HttpClient (Java 11+), or in a small @Aspect around every @Scheduled method if you have many. Register the agent's cadence once.
Store the key
RUNVOUCH_KEY as an environment variable read through @Value("${RUNVOUCH_KEY}") or application.yml with a placeholder; Spring Cloud Config or Vault if you have them.
Report the run (two HTTP calls)
@Component
public class NightlyReport {
private static final String API = "https://api.runvouch.com";
private final HttpClient http = HttpClient.newHttpClient();
private final ObjectMapper json = new ObjectMapper();
@Value("${RUNVOUCH_KEY}") String key;
private Map<String, Object> post(String path, Map<String, Object> body) {
try {
var req = HttpRequest.newBuilder(URI.create(API + path)).timeout(Duration.ofSeconds(10))
.header("X-API-Key", key).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json.writeValueAsString(body))).build();
return json.readValue(http.send(req, HttpResponse.BodyHandlers.ofString()).body(), Map.class);
} catch (Exception e) { return Map.of(); } // fail open: the job still runs
}
@Scheduled(cron = "0 0 2 * * *", zone = "UTC")
public void run() {
String runId = (String) post("/v1/runs/start", Map.of("agent", "nightly-report", "source", "spring")).get("run_id");
try {
int rows = reportService.build(); // your work
post("/v1/runs/end", Map.of("run_id", runId, "status", "ok", "evidence", Map.of("rows_written", rows > 0)));
} catch (Exception e) {
post("/v1/runs/end", Map.of("run_id", runId, "status", "fail", "meta", Map.of("error", e.toString())));
throw e;
}
}
}
// @EnableScheduling on a configuration class; rv agent nightly-report --cadence 24h --grace 30m --max-runtime 1h --evidenceThe start call returns run_id; the end call takes status (ok or fail), optional cost and tokens, and evidence as a JSON object of booleans. Full reference on the API page.
Register the cadence and caps
rv agent nightly-report --cadence 24h --grace 30m --max-runtime 1h --evidence --cap-run-cost 2
Register the agent once, from anywhere with the key. Cadence is what turns a schedule that stopped into an alert; --evidence makes a run without evidence a failure; the caps pause the agent when it overspends.
What goes silent on Spring @Scheduled
- The default
TaskSchedulerhas one thread. A task blocked on a slow HTTP call without a timeout blocks every other@Scheduledmethod; the blocked one is STALLED, the others are MISSED, and the app's health endpoint says UP. - Exceptions are handled by the scheduler's
ErrorHandler(logged) and the schedule continues; nothing is thrown to anything that would page you. - No catch-up: a fire time that passes while the app is restarting is skipped.
- Two instances of the app run the task twice unless you add ShedLock or similar; both runs are visible under one agent.
What Spring @Scheduled does not tell you
Spring logs a scheduled task's exception at ERROR level through the default error handler and moves on; Actuator's /actuator/scheduledtasks lists the schedules, not their outcomes, and Micrometer only measures what you instrument. Nothing in the framework knows that 02:00 came and went with the app down, or that the method returned after writing an empty file.
What RunVouch detects
| Alert | What it means here |
|---|---|
| MISSED | cadence plus grace passed and no run started |
| FAILED | non-zero exit or a reported failure, with the stderr excerpt |
| NO_EVIDENCE | the run said ok but the file, URL or assertion you required is missing |
| STALLED | a run started and never ended within max runtime |
| RETRY_STORM | the same tool called with identical input many times in one run |
| BUDGET_RUN / BUDGET_DAY | cost cap crossed; the agent is paused until you resume it |
| DRIFT | duration or output size far off its 7-run baseline |
Set up in two minutes
- Get a free key (3 agents, no card) and store it where this page says.
- report the run (two http calls): copy the snippet above into the scheduled job.
- Register the cadence once:
rv agent nightly-report --cadence 24h --grace 30m --evidence, or let the first run create the agent and set the cadence on the dashboard. - Send one test alert:
curl -X POST https://api.runvouch.com/v1/settings/test-alert -H "X-API-Key: $RUNVOUCH_KEY". The next missed, failed or empty run reaches the same channels.
Need a key? Get a free key · Stuck? contact