Skip to content

Commit 74c3b15

Browse files
marcingrzejszczaklcmarvin
authored andcommitted
Add Observation API
Issue spring-projects#4065
1 parent 0582485 commit 74c3b15

File tree

22 files changed

+670
-35
lines changed

22 files changed

+670
-35
lines changed

pom.xml

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
<spring-retry.version>1.3.1</spring-retry.version>
5656
<spring-integration.version>6.0.0-SNAPSHOT</spring-integration.version>
5757
<micrometer.version>2.0.0-SNAPSHOT</micrometer.version>
58+
<micrometer-tracing.version>1.0.0-SNAPSHOT</micrometer-tracing.version>
5859
<jackson.version>2.13.1</jackson.version>
5960

6061
<!-- optional production dependencies -->

spring-batch-core/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,12 @@
256256
<version>${jakarta.inject-api.version}</version>
257257
<scope>test</scope>
258258
</dependency>
259+
<dependency>
260+
<groupId>io.micrometer</groupId>
261+
<artifactId>micrometer-test</artifactId>
262+
<version>${micrometer.version}</version>
263+
<scope>test</scope>
264+
</dependency>
259265
</dependencies>
260266

261267
</project>

spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java

+32-11
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818

1919
import java.util.Collection;
2020
import java.util.Date;
21+
import java.util.List;
22+
import java.util.stream.Collectors;
2123

22-
import io.micrometer.api.instrument.LongTaskTimer;
23-
import io.micrometer.api.instrument.Tag;
24-
import io.micrometer.api.instrument.Timer;
24+
import io.micrometer.core.instrument.LongTaskTimer;
25+
import io.micrometer.core.instrument.Tag;
26+
import io.micrometer.core.instrument.observation.Observation;
2527
import org.apache.commons.logging.Log;
2628
import org.apache.commons.logging.LogFactory;
2729
import org.springframework.batch.core.BatchStatus;
@@ -62,7 +64,7 @@
6264
* @author Mahmoud Ben Hassine
6365
*/
6466
public abstract class AbstractJob implements Job, StepLocator, BeanNameAware,
65-
InitializingBean {
67+
InitializingBean, Observation.TagsProviderAware<BatchJobTagsProvider> {
6668

6769
protected static final Log logger = LogFactory.getLog(AbstractJob.class);
6870

@@ -80,6 +82,8 @@ public abstract class AbstractJob implements Job, StepLocator, BeanNameAware,
8082

8183
private StepHandler stepHandler;
8284

85+
private BatchJobTagsProvider tagsProvider = new DefaultBatchJobTagsProvider();
86+
8387
/**
8488
* Default constructor.
8589
*/
@@ -304,8 +308,11 @@ public final void execute(JobExecution execution) {
304308
LongTaskTimer longTaskTimer = BatchMetrics.createLongTaskTimer("job.active", "Active jobs",
305309
Tag.of("name", execution.getJobInstance().getJobName()));
306310
LongTaskTimer.Sample longTaskTimerSample = longTaskTimer.start();
307-
Timer.Sample timerSample = BatchMetrics.createTimerSample();
308-
try {
311+
Observation observation = BatchMetrics.createObservation(BatchJobObservation.BATCH_JOB_OBSERVATION.getName(), new BatchJobContext(execution))
312+
.contextualName(execution.getJobInstance().getJobName())
313+
.tagsProvider(this.tagsProvider)
314+
.start();
315+
try (Observation.Scope scope = observation.openScope()) {
309316

310317
jobParametersValidator.validate(execution.getJobParameters());
311318

@@ -361,11 +368,7 @@ public final void execute(JobExecution execution) {
361368
ExitStatus.NOOP.addExitDescription("All steps already completed or no steps configured for this job.");
362369
execution.setExitStatus(exitStatus.and(newExitStatus));
363370
}
364-
365-
timerSample.stop(BatchMetrics.createTimer("job", "Job duration",
366-
Tag.of("name", execution.getJobInstance().getJobName()),
367-
Tag.of("status", execution.getExitStatus().getExitCode())
368-
));
371+
stopObservation(execution, observation);
369372
longTaskTimerSample.stop();
370373
execution.setEndTime(new Date());
371374

@@ -384,6 +387,19 @@ public final void execute(JobExecution execution) {
384387

385388
}
386389

390+
private void stopObservation(JobExecution execution, Observation observation) {
391+
List<Throwable> throwables = execution.getFailureExceptions();
392+
if (!throwables.isEmpty()) {
393+
observation.error(mergedThrowables(throwables));
394+
}
395+
observation.stop();
396+
}
397+
398+
private IllegalStateException mergedThrowables(List<Throwable> throwables) {
399+
return new IllegalStateException(
400+
throwables.stream().map(Throwable::toString).collect(Collectors.joining("\n")));
401+
}
402+
387403
/**
388404
* Convenience method for subclasses to delegate the handling of a specific
389405
* step in the context of the current {@link JobExecution}. Clients of this
@@ -443,6 +459,11 @@ private void updateStatus(JobExecution jobExecution, BatchStatus status) {
443459
jobRepository.update(jobExecution);
444460
}
445461

462+
@Override
463+
public void setTagsProvider(BatchJobTagsProvider tagsProvider) {
464+
this.tagsProvider = tagsProvider;
465+
}
466+
446467
@Override
447468
public String toString() {
448469
return ClassUtils.getShortName(getClass()) + ": [name=" + name + "]";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2013-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.batch.core.job;
18+
19+
import io.micrometer.core.instrument.observation.Observation;
20+
21+
import org.springframework.batch.core.JobExecution;
22+
23+
public class BatchJobContext extends Observation.Context {
24+
25+
private final JobExecution jobExecution;
26+
27+
public BatchJobContext(JobExecution jobExecution) {
28+
this.jobExecution = jobExecution;
29+
}
30+
31+
public JobExecution getJobExecution() {
32+
return jobExecution;
33+
}
34+
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2013-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.batch.core.job;
18+
19+
import io.micrometer.core.instrument.docs.DocumentedObservation;
20+
import io.micrometer.core.instrument.docs.TagKey;
21+
22+
enum BatchJobObservation implements DocumentedObservation {
23+
24+
/**
25+
* Observation created around a Job execution.
26+
*/
27+
BATCH_JOB_OBSERVATION {
28+
@Override
29+
public String getName() {
30+
return "spring.batch.job";
31+
}
32+
33+
@Override
34+
public String getContextualName() {
35+
return "%s";
36+
}
37+
38+
@Override
39+
public TagKey[] getLowCardinalityTagKeys() {
40+
return JobLowCardinalityTags.values();
41+
}
42+
43+
@Override
44+
public TagKey[] getHighCardinalityTagKeys() {
45+
return JobHighCardinalityTags.values();
46+
}
47+
48+
@Override
49+
public String getPrefix() {
50+
return "spring.batch";
51+
}
52+
};
53+
54+
enum JobLowCardinalityTags implements TagKey {
55+
56+
/**
57+
* Name of the Spring Batch job.
58+
*/
59+
JOB_NAME {
60+
@Override
61+
public String getKey() {
62+
return "spring.batch.job.name";
63+
}
64+
},
65+
66+
/**
67+
* Job status.
68+
*/
69+
JOB_STATUS {
70+
@Override
71+
public String getKey() {
72+
return "spring.batch.job.status";
73+
}
74+
}
75+
76+
}
77+
78+
enum JobHighCardinalityTags implements TagKey {
79+
80+
/**
81+
* ID of the Spring Batch job instance.
82+
*/
83+
JOB_INSTANCE_ID {
84+
@Override
85+
public String getKey() {
86+
return "spring.batch.job.instanceId";
87+
}
88+
},
89+
90+
/**
91+
* ID of the Spring Batch execution.
92+
*/
93+
JOB_EXECUTION_ID {
94+
@Override
95+
public String getKey() {
96+
return "spring.batch.job.executionId";
97+
}
98+
}
99+
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2006-2009 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.batch.core.job;
18+
19+
import io.micrometer.core.instrument.observation.Observation;
20+
21+
import org.springframework.batch.core.Job;
22+
import org.springframework.batch.core.JobExecution;
23+
import org.springframework.batch.core.JobInterruptedException;
24+
import org.springframework.batch.core.StartLimitExceededException;
25+
import org.springframework.batch.core.Step;
26+
import org.springframework.batch.core.StepExecution;
27+
import org.springframework.batch.core.repository.JobRestartException;
28+
29+
/**
30+
* {@link Observation.TagsProvider} for {@link BatchJobContext}.
31+
*
32+
* @author Marcin Grzejszczak
33+
*/
34+
public interface BatchJobTagsProvider extends Observation.TagsProvider<BatchJobContext> {
35+
36+
@Override
37+
default boolean supportsContext(Observation.Context context) {
38+
return context instanceof BatchJobContext;
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2011-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.core.job;
17+
18+
import io.micrometer.core.instrument.Tags;
19+
20+
import org.springframework.batch.core.JobExecution;
21+
22+
/**
23+
* Default {@link BatchJobTagsProvider} implementation.
24+
*
25+
* @author Marcin Grzejszczak
26+
*/
27+
public class DefaultBatchJobTagsProvider implements BatchJobTagsProvider {
28+
@Override
29+
public Tags getLowCardinalityTags(BatchJobContext context) {
30+
JobExecution execution = context.getJobExecution();
31+
return Tags.of(BatchJobObservation.JobLowCardinalityTags.JOB_NAME.of(execution.getJobInstance().getJobName()),
32+
BatchJobObservation.JobLowCardinalityTags.JOB_STATUS.of(execution.getExitStatus().getExitCode()));
33+
}
34+
35+
@Override
36+
public Tags getHighCardinalityTags(BatchJobContext context) {
37+
JobExecution execution = context.getJobExecution();
38+
return Tags.of(BatchJobObservation.JobHighCardinalityTags.JOB_INSTANCE_ID.of(String.valueOf(execution.getJobInstance().getInstanceId())),
39+
BatchJobObservation.JobHighCardinalityTags.JOB_EXECUTION_ID.of(String.valueOf(execution.getId())));
40+
}
41+
42+
}

spring-batch-core/src/main/java/org/springframework/batch/core/metrics/BatchMetrics.java

+19-4
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
import java.util.Date;
2121
import java.util.concurrent.TimeUnit;
2222

23-
import io.micrometer.api.instrument.LongTaskTimer;
24-
import io.micrometer.api.instrument.Metrics;
25-
import io.micrometer.api.instrument.Tag;
26-
import io.micrometer.api.instrument.Timer;
23+
import io.micrometer.core.instrument.LongTaskTimer;
24+
import io.micrometer.core.instrument.Metrics;
25+
import io.micrometer.core.instrument.Tag;
26+
import io.micrometer.core.instrument.Timer;
27+
import io.micrometer.core.instrument.observation.Observation;
28+
import io.micrometer.core.instrument.observation.TimerObservationHandler;
2729

2830
import org.springframework.lang.Nullable;
2931

@@ -66,6 +68,19 @@ public static Timer createTimer(String name, String description, Tag... tags) {
6668
.register(Metrics.globalRegistry);
6769
}
6870

71+
/**
72+
* Create a new {@link Observation}. It's not started, you must
73+
* explicitly call {@link Observation#start()} to start it.
74+
*
75+
* Remember to register the {@link TimerObservationHandler}
76+
* via the {@code Metrics.globalRegistry.withTimerObservationHandler()}
77+
* in the user code. Otherwise you won't observe any metrics.
78+
* @return a new observation instance
79+
*/
80+
public static Observation createObservation(String name, Observation.Context context) {
81+
return Observation.createNotStarted(name, context, Metrics.globalRegistry);
82+
}
83+
6984
/**
7085
* Create a new {@link Timer.Sample}.
7186
* @return a new timer sample instance

0 commit comments

Comments
 (0)