diff --git a/src/main/java/com/template/worker/global/config/BatchMetricsConfig.java b/src/main/java/com/template/worker/global/config/BatchMetricsConfig.java new file mode 100644 index 0000000..b3a1326 --- /dev/null +++ b/src/main/java/com/template/worker/global/config/BatchMetricsConfig.java @@ -0,0 +1,81 @@ +package com.template.worker.global.config; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; + +import lombok.RequiredArgsConstructor; + +@Configuration +@RequiredArgsConstructor +public class BatchMetricsConfig { + private final MeterRegistry meterRegistry; + + // Active jobs gauge + @Bean + public AtomicInteger activeJobsGauge() { + return meterRegistry.gauge("spring.batch.job.active", new AtomicInteger(0)); + } + + // Completed/Failed job counters + @Bean + public Counter jobCompletedCounter() { + return Counter.builder("spring.batch.job.completed.total") + .description("Total completed batch jobs") + .register(meterRegistry); + } + + @Bean + public Counter jobFailedCounter() { + return Counter.builder("spring.batch.job.failed.total") + .description("Total failed batch jobs") + .register(meterRegistry); + } + + // Partition count by status + @Bean + public Counter partitionCompletedCounter() { + return Counter.builder("spring.batch.partition.count") + .tag("status", "COMPLETED") + .description("Total completed partitions") + .register(meterRegistry); + } + + @Bean + public Counter partitionFailedCounter() { + return Counter.builder("spring.batch.partition.count") + .tag("status", "FAILED") + .description("Total failed partitions") + .register(meterRegistry); + } + + // Chunk count + @Bean + public Counter chunkCounter() { + return Counter.builder("spring.batch.chunk.count") + .description("Total chunks processed") + .register(meterRegistry); + } + + // Step item count + @Bean + public Counter stepItemCounter() { + return Counter.builder("spring.batch.step.item.count") + .description("Total items processed by steps") + .register(meterRegistry); + } + + // Step duration histogram + @Bean + public Timer stepDurationTimer() { + return Timer.builder("spring.batch.step.duration.seconds") + .publishPercentileHistogram() + .description("Step duration histogram") + .register(meterRegistry); + } +} diff --git a/src/main/java/com/template/worker/global/listener/JobResultListener.java b/src/main/java/com/template/worker/global/listener/JobResultListener.java index 3ae0dca..0ef2964 100644 --- a/src/main/java/com/template/worker/global/listener/JobResultListener.java +++ b/src/main/java/com/template/worker/global/listener/JobResultListener.java @@ -2,6 +2,7 @@ import java.time.Duration; import java.time.LocalDateTime; +import java.util.concurrent.atomic.AtomicInteger; import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.ExitStatus; @@ -11,6 +12,8 @@ import org.springframework.batch.item.ExecutionContext; import org.springframework.stereotype.Component; +import io.micrometer.core.instrument.Counter; + import lombok.RequiredArgsConstructor; @Component @@ -21,12 +24,19 @@ public class JobResultListener { private static final ExitStatus NO_DATA_EXIT_STATUS = new ExitStatus("NO_DATA", "⚠ 데이터가 없습니다."); private final JobLogger jobLogger; + private final AtomicInteger activeJobsGauge; + private final Counter jobCompletedCounter; + private final Counter jobFailedCounter; @BeforeJob - public void before(JobExecution jobExecution) {} + public void before(JobExecution jobExecution) { + activeJobsGauge.incrementAndGet(); + } @AfterJob public void after(JobExecution jobExecution) { + activeJobsGauge.decrementAndGet(); + ExecutionContext context = jobExecution.getExecutionContext(); String jobName = jobExecution.getJobInstance().getJobName(); @@ -42,6 +52,7 @@ public void after(JobExecution jobExecution) { if (!hasData) { jobExecution.setStatus(BatchStatus.FAILED); jobExecution.setExitStatus(NO_DATA_EXIT_STATUS); + jobFailedCounter.increment(); jobLogger.jobFailed( jobName, @@ -56,9 +67,11 @@ public void after(JobExecution jobExecution) { jobExecution.getAllFailureExceptions().isEmpty() ? null : jobExecution.getAllFailureExceptions().get(0); + jobFailedCounter.increment(); jobLogger.jobFailed(jobName, duration, cause); } else { + jobCompletedCounter.increment(); jobLogger.jobSuccess(jobName, duration); } } diff --git a/src/main/java/com/template/worker/global/listener/PartitionTimingListener.java b/src/main/java/com/template/worker/global/listener/PartitionTimingListener.java index 3a2b6bc..7ed551e 100644 --- a/src/main/java/com/template/worker/global/listener/PartitionTimingListener.java +++ b/src/main/java/com/template/worker/global/listener/PartitionTimingListener.java @@ -1,19 +1,32 @@ package com.template.worker.global.listener; +import java.util.concurrent.TimeUnit; + +import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.StepExecutionListener; import org.springframework.stereotype.Component; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Timer; + +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @Component +@RequiredArgsConstructor public class PartitionTimingListener implements StepExecutionListener { private static final String START_TIME = "startTime"; private static final String HAS_DATA = "HAS_DATA"; + private final Counter partitionCompletedCounter; + private final Counter partitionFailedCounter; + private final Counter stepItemCounter; + private final Timer stepDurationTimer; + @Override public void beforeStep(StepExecution stepExecution) { stepExecution.getExecutionContext().putLong(START_TIME, System.currentTimeMillis()); @@ -39,6 +52,15 @@ public ExitStatus afterStep(StepExecution stepExecution) { stepExecution.getJobExecution().getExecutionContext().put(HAS_DATA, true); } + // Record Prometheus metrics + if (stepExecution.getStatus() == BatchStatus.COMPLETED) { + partitionCompletedCounter.increment(); + } else if (stepExecution.getStatus() == BatchStatus.FAILED) { + partitionFailedCounter.increment(); + } + stepItemCounter.increment(readCount); + stepDurationTimer.record(duration, TimeUnit.MILLISECONDS); + return stepExecution.getExitStatus(); } } diff --git a/src/main/java/com/template/worker/global/listener/TimeBasedChunkListener.java b/src/main/java/com/template/worker/global/listener/TimeBasedChunkListener.java index 0d9b045..97a5f88 100644 --- a/src/main/java/com/template/worker/global/listener/TimeBasedChunkListener.java +++ b/src/main/java/com/template/worker/global/listener/TimeBasedChunkListener.java @@ -6,20 +6,29 @@ import org.springframework.batch.item.ExecutionContext; import org.springframework.stereotype.Component; +import io.micrometer.core.instrument.Counter; + +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Component @Slf4j +@RequiredArgsConstructor public class TimeBasedChunkListener implements ChunkListener { private static final long LOG_INTERVAL_MS = 10_000; private static final String LAST_LOG_TIME_KEY = "lastLogTime"; + private final Counter chunkCounter; + @Override public void afterChunk(ChunkContext context) { StepExecution stepExecution = context.getStepContext().getStepExecution(); ExecutionContext executionContext = stepExecution.getExecutionContext(); + // Record chunk metric + chunkCounter.increment(); + long now = System.currentTimeMillis(); long lastLogTime = executionContext.containsKey(LAST_LOG_TIME_KEY)