1919import java .util .List ;
2020import java .util .concurrent .Future ;
2121
22+ import io .micrometer .core .instrument .MeterRegistry ;
23+ import io .micrometer .core .instrument .Metrics ;
24+ import io .micrometer .core .instrument .Tag ;
25+ import io .micrometer .core .instrument .Timer ;
2226import org .apache .commons .logging .Log ;
2327import org .apache .commons .logging .LogFactory ;
2428import org .jspecify .annotations .Nullable ;
3539import org .springframework .batch .core .listener .ItemWriteListener ;
3640import org .springframework .batch .core .listener .SkipListener ;
3741import org .springframework .batch .core .observability .BatchMetrics ;
38- import org .springframework .batch .core .observability .jfr .events .step .chunk .*;
42+ import org .springframework .batch .core .observability .jfr .events .step .chunk .ChunkScanEvent ;
43+ import org .springframework .batch .core .observability .jfr .events .step .chunk .ChunkTransactionEvent ;
44+ import org .springframework .batch .core .observability .jfr .events .step .chunk .ChunkWriteEvent ;
45+ import org .springframework .batch .core .observability .jfr .events .step .chunk .ItemProcessEvent ;
46+ import org .springframework .batch .core .observability .jfr .events .step .chunk .ItemReadEvent ;
3947import org .springframework .batch .core .scope .context .StepContext ;
4048import org .springframework .batch .core .scope .context .StepSynchronizationManager ;
4149import org .springframework .batch .core .step .StepContribution ;
6775import org .springframework .transaction .TransactionStatus ;
6876import org .springframework .transaction .interceptor .DefaultTransactionAttribute ;
6977import org .springframework .transaction .interceptor .TransactionAttribute ;
70- import org .springframework .transaction .support .TransactionCallbackWithoutResult ;
7178import org .springframework .transaction .support .TransactionTemplate ;
7279import org .springframework .util .Assert ;
7380
@@ -146,6 +153,11 @@ public class ChunkOrientedStep<I, O> extends AbstractStep {
146153 */
147154 private AsyncTaskExecutor taskExecutor ;
148155
156+ /*
157+ * Observability parameters
158+ */
159+ private MeterRegistry meterRegistry ;
160+
149161 /**
150162 * Create a new {@link ChunkOrientedStep}.
151163 * @param name the name of the step
@@ -306,6 +318,16 @@ public void registerSkipListener(SkipListener<I, O> skipListener) {
306318 this .compositeSkipListener .register (skipListener );
307319 }
308320
321+ /**
322+ * Set the meter registry to use for metrics.
323+ * @param meterRegistry the meter registry
324+ * @since 6.0
325+ */
326+ public void setMeterRegistry (MeterRegistry meterRegistry ) {
327+ Assert .notNull (meterRegistry , "Meter registry must not be null" );
328+ this .meterRegistry = meterRegistry ;
329+ }
330+
309331 @ Override
310332 public void afterPropertiesSet () throws Exception {
311333 super .afterPropertiesSet ();
@@ -335,6 +357,10 @@ public void afterPropertiesSet() throws Exception {
335357 this .retryTemplate .setRetryPolicy (this .retryPolicy );
336358 this .retryTemplate .setRetryListener (this .compositeRetryListener );
337359 }
360+ if (this .meterRegistry == null ) {
361+ logger .info ("No meter registry has been set. Defaulting to the global meter registry." );
362+ this .meterRegistry = Metrics .globalRegistry ;
363+ }
338364 }
339365
340366 @ Override
@@ -481,6 +507,8 @@ private Chunk<I> readChunk(StepContribution contribution) throws Exception {
481507 @ Nullable private I readItem (StepContribution contribution ) throws Exception {
482508 ItemReadEvent itemReadEvent = new ItemReadEvent (contribution .getStepExecution ().getStepName (),
483509 contribution .getStepExecution ().getId ());
510+ Timer .Sample sample = startTimerSample ();
511+ String status = BatchMetrics .STATUS_SUCCESS ;
484512 I item = null ;
485513 try {
486514 itemReadEvent .begin ();
@@ -504,9 +532,12 @@ private Chunk<I> readChunk(StepContribution contribution) throws Exception {
504532 throw exception ;
505533 }
506534 itemReadEvent .itemReadStatus = BatchMetrics .STATUS_FAILURE ;
535+ status = BatchMetrics .STATUS_FAILURE ;
507536 }
508537 finally {
509538 itemReadEvent .commit ();
539+ stopTimerSample (sample , contribution .getStepExecution ().getJobExecution ().getJobInstance ().getJobName (),
540+ contribution .getStepExecution ().getStepName (), "item.read" , "Item reading" , status );
510541 }
511542 return item ;
512543 }
@@ -558,6 +589,8 @@ private Chunk<O> processChunk(Chunk<I> chunk, StepContribution contribution) thr
558589 private O processItem (I item , StepContribution contribution ) throws Exception {
559590 ItemProcessEvent itemProcessEvent = new ItemProcessEvent (contribution .getStepExecution ().getStepName (),
560591 contribution .getStepExecution ().getId ());
592+ Timer .Sample sample = startTimerSample ();
593+ String status = BatchMetrics .STATUS_SUCCESS ;
561594 O processedItem = null ;
562595 try {
563596 itemProcessEvent .begin ();
@@ -578,9 +611,12 @@ private O processItem(I item, StepContribution contribution) throws Exception {
578611 throw exception ;
579612 }
580613 itemProcessEvent .itemProcessStatus = BatchMetrics .STATUS_FAILURE ;
614+ status = BatchMetrics .STATUS_FAILURE ;
581615 }
582616 finally {
583617 itemProcessEvent .commit ();
618+ stopTimerSample (sample , contribution .getStepExecution ().getJobExecution ().getJobInstance ().getJobName (),
619+ contribution .getStepExecution ().getStepName (), "item.process" , "Item processing" , status );
584620 }
585621 return processedItem ;
586622 }
@@ -633,6 +669,8 @@ private void doSkipInProcess(I item, RetryException retryException, StepContribu
633669 private void writeChunk (Chunk <O > chunk , StepContribution contribution ) throws Exception {
634670 ChunkWriteEvent chunkWriteEvent = new ChunkWriteEvent (contribution .getStepExecution ().getStepName (),
635671 contribution .getStepExecution ().getId (), chunk .size ());
672+ Timer .Sample sample = startTimerSample ();
673+ String status = BatchMetrics .STATUS_SUCCESS ;
636674 try {
637675 chunkWriteEvent .begin ();
638676 this .compositeItemWriteListener .beforeWrite (chunk );
@@ -644,6 +682,7 @@ private void writeChunk(Chunk<O> chunk, StepContribution contribution) throws Ex
644682 catch (Exception exception ) {
645683 this .compositeItemWriteListener .onWriteError (exception , chunk );
646684 chunkWriteEvent .chunkWriteStatus = BatchMetrics .STATUS_FAILURE ;
685+ status = BatchMetrics .STATUS_FAILURE ;
647686 if (this .faultTolerant && exception instanceof RetryException retryException ) {
648687 logger .info ("Retry exhausted while attempting to write items, scanning the chunk" , retryException );
649688 ChunkScanEvent chunkScanEvent = new ChunkScanEvent (contribution .getStepExecution ().getStepName (),
@@ -660,6 +699,8 @@ private void writeChunk(Chunk<O> chunk, StepContribution contribution) throws Ex
660699 }
661700 finally {
662701 chunkWriteEvent .commit ();
702+ stopTimerSample (sample , contribution .getStepExecution ().getJobExecution ().getJobInstance ().getJobName (),
703+ contribution .getStepExecution ().getStepName (), "chunk.write" , "Chunk writing" , status );
663704 }
664705 }
665706
@@ -711,6 +752,19 @@ private void scan(Chunk<O> chunk, StepContribution contribution) {
711752 }
712753 }
713754
755+ private Timer .Sample startTimerSample () {
756+ return BatchMetrics .createTimerSample (this .meterRegistry );
757+ }
758+
759+ private void stopTimerSample (Timer .Sample sample , String jobName , String stepName , String operation ,
760+ String description , String status ) {
761+ String fullyQualifiedMetricName = BatchMetrics .METRICS_PREFIX + operation ;
762+ sample .stop (BatchMetrics .createTimer (this .meterRegistry , operation , description + " duration" ,
763+ Tag .of (fullyQualifiedMetricName + ".job.name" , jobName ),
764+ Tag .of (fullyQualifiedMetricName + ".step.name" , stepName ),
765+ Tag .of (fullyQualifiedMetricName + ".status" , status )));
766+ }
767+
714768 private boolean isConcurrent () {
715769 return this .taskExecutor != null ;
716770 }
0 commit comments