Observability
|
This section documents the current Spring Batch line — 6.0.x, on Spring Framework 7 and Spring Boot 4.1.x, with a Java 17+ baseline — as published at the Spring Batch reference documentation. No specific patch version is pinned. Some surfaces (Spring Cloud Task and Spring Cloud Data Flow orchestration, the deployer-based partition handler, and JSR-352) are linked, not documented in depth. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. |
A batch job that runs at 02:00 is only as good as what it reports. Spring Batch instruments itself with Micrometer and the Observation API, so job, step and chunk timings reach the same monitoring stack as the rest of the application.
Micrometer meters
Spring Batch publishes these meters under the spring.batch prefix:
| Meter | Type | Records |
|---|---|---|
|
Timer |
Duration of a whole job execution. Tags: |
|
LongTaskTimer |
Currently running job executions and how long each has been running — the meter that answers "is last night’s job still going?". |
|
Timer |
Duration of a step execution. Tags: |
|
Timer |
Duration of a single |
|
Timer |
Duration of a single |
|
Timer |
Duration of one |
The split between item-level and chunk-level timers is what makes a slow step diagnosable: if
spring.batch.item.process dominates, the processor is the problem; if spring.batch.chunk.write dominates,
the writer or the database is. That is the same question
Profiling & tuning starts from, answered continuously
instead of in a profiling session.
Under Spring Boot, a MeterRegistry on the classpath is enough — the meters are registered automatically:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: batch-app
Outside Spring Boot, register a global registry explicitly:
@Configuration
public class MetricsConfiguration {
@Bean
public MeterRegistry meterRegistry() {
PrometheusMeterRegistry registry =
new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
Metrics.addRegistry(registry); // Spring Batch publishes to the global registry
return registry;
}
}
Business-level counters belong alongside them — a rejected-record counter incremented from a SkipListener
(Fault tolerance: skip & retry) is often the
most actionable metric a batch job has:
@Component
public class MeteredSkipListener implements SkipListener<TradeCsv, Trade> {
private final Counter rejected;
public MeteredSkipListener(MeterRegistry registry) {
this.rejected = Counter.builder("batch.trades.rejected")
.description("trades skipped during the load step")
.register(registry);
}
@Override
public void onSkipInProcess(TradeCsv item, Throwable throwable) {
rejected.increment();
}
}
A useful alerting rule set: job duration above its usual band, spring.batch.job.active still non-zero past a
deadline, any execution with status=FAILED, and a rejection counter above a threshold.
Tracing with the Observation API
Every meter above is produced by an Observation, so the same instrumentation also emits spans when a tracer is present: one observation per job execution, one per step execution, and one per chunk. A job that calls downstream services therefore appears in a trace as a job span containing step spans containing the outgoing HTTP or messaging spans.
@Configuration
public class ObservabilityConfiguration {
@Bean
public ObservationRegistry observationRegistry(MeterRegistry meterRegistry) {
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig()
.observationHandler(new DefaultMeterObservationHandler(meterRegistry));
return registry;
}
}
Under Spring Boot, spring-boot-starter-actuator plus a tracer bridge (micrometer-tracing-bridge-otel and
an exporter) is all that is needed; the general Micrometer / OpenTelemetry / Prometheus / Grafana setup is
covered on Metrics & Observability and is not restated
here.
Sampling deserves a thought for batch: a chunk observation per 500 items is fine, but item-level spans on a ten-million-row job are not. Sample aggressively, or rely on the timers rather than on spans for item-level detail.
Java Flight Recorder events
Spring Batch 6.0 emits JFR events for job, step and chunk execution. Because JFR is built into the JVM and cheap enough to leave on, this gives a full picture of a production run — framework phases interleaved with GC, allocation, JDBC and thread activity — without attaching anything:
java -XX:StartFlightRecording=filename=endofday.jfr,settings=profile,dumponexit=true \
-jar batch-app.jar --spring.batch.job.name=endOfDayJob run.date=2026-01-31
jfr summary endofday.jfr
jfr print --events SpringBatchJobExecution,SpringBatchStepExecution endofday.jfr
This is the tool of choice for "the job took three hours last night and nobody was watching": the recording is written by the run itself and analysed afterwards in JDK Mission Control.
What Actuator adds
With spring-boot-starter-actuator on the classpath, a batch application also gets:
-
/actuator/metrics/spring.batch.joband the other meters, browsable per tag; -
/actuator/prometheusfor scraping; -
/actuator/health— worth extending with a custom indicator that reports the last execution’s status, so an orchestrator can tell a failed job from a finished one; -
/actuator/loggersto raise a package’s log level on a running job without restarting it.
@Component
public class LastRunHealthIndicator implements HealthIndicator {
private final JobRepository jobRepository;
public LastRunHealthIndicator(JobRepository jobRepository) {
this.jobRepository = jobRepository;
}
@Override
public Health health() {
JobInstance instance = jobRepository.getLastJobInstance("endOfDayJob");
if (instance == null) {
return Health.unknown().withDetail("reason", "never run").build();
}
JobExecution execution = jobRepository.getLastJobExecution(instance);
return (execution.getStatus() == BatchStatus.COMPLETED ? Health.up() : Health.down())
.withDetail("executionId", execution.getId())
.withDetail("status", execution.getStatus())
.withDetail("endTime", execution.getEndTime())
.build();
}
}
The metadata tables remain the durable record behind all of this — see The job repository & metadata schema.