Instrument spring-batch: item-level spans
MODIFY
instrumentation/spring/spring-batch-3.0/javaagent/spring-batch-3.0-javaagent.gradle
instrumentation/spring/spring-batch-3.0/javaagent/spring-batch-3.0-javaagent.gradle
@@ -23,6 +23,7 @@ tasks.withType(Test) {
test {
filter {
excludeTestsMatching '*ChunkRootSpanTest'
+ excludeTestsMatching '*ItemLevelSpanTest'
}
}
test.finalizedBy(tasks.register("testChunkRootSpan", Test) {
@@ -30,4 +31,9 @@ test.finalizedBy(tasks.register("testChunkRootSpan", Test) {
includeTestsMatching '*ChunkRootSpanTest'
}
jvmArgs '-Dotel.instrumentation.spring-batch.chunk.root-span=true'
+}).finalizedBy(tasks.register("testItemLevelSpan", Test) {
+ filter {
+ includeTestsMatching '*ItemLevelSpanTest'
+ }
+ jvmArgs '-Dotel.instrumentation.spring-batch.item.enabled=true'
})

MODIFY
instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/SpringBatchInstrumentationConfig.java
instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/SpringBatchInstrumentationConfig.java
@@ -15,18 +15,23 @@ public final class SpringBatchInstrumentationConfig {
private static final List<String> INSTRUMENTATION_NAMES =
unmodifiableList(asList("spring-batch", "spring-batch-3.0"));
+ // the item level instrumentation is very chatty so it's disabled by default
+ private static final boolean ITEM_TRACING_ENABLED =
+ Config.get().isInstrumentationPropertyEnabled(instrumentationNames(), "item.enabled", false);
+ private static final boolean CREATE_ROOT_SPAN_FOR_CHUNK =
+ Config.get()
+ .isInstrumentationPropertyEnabled(instrumentationNames(), "chunk.root-span", false);
+
public static List<String> instrumentationNames() {
return INSTRUMENTATION_NAMES;
}
- public static boolean isTracingEnabled(String type) {
- return Config.get()
- .isInstrumentationPropertyEnabled(instrumentationNames(), type + ".enabled", true);
+ public static boolean shouldTraceItems() {
+ return ITEM_TRACING_ENABLED;
}
public static boolean shouldCreateRootSpanForChunk() {
- return Config.get()
- .isInstrumentationPropertyEnabled(instrumentationNames(), "chunk.root-span", false);
+ return CREATE_ROOT_SPAN_FOR_CHUNK;
}
private SpringBatchInstrumentationConfig() {}

MODIFY
instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/SpringBatchInstrumentationModule.java
instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/SpringBatchInstrumentationModule.java
package io.opentelemetry.javaagent.instrumentation.spring.batch;
import static io.opentelemetry.javaagent.instrumentation.spring.batch.SpringBatchInstrumentationConfig.instrumentationNames;
-import static io.opentelemetry.javaagent.instrumentation.spring.batch.SpringBatchInstrumentationConfig.isTracingEnabled;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.ClassLoaderMatcher.hasClassesNamed;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.instrumentation.spring.batch.chunk.StepBuilderInstrumentation;
+import io.opentelemetry.javaagent.instrumentation.spring.batch.item.ChunkOrientedTaskletInstrumentation;
+import io.opentelemetry.javaagent.instrumentation.spring.batch.item.JsrChunkProcessorInstrumentation;
+import io.opentelemetry.javaagent.instrumentation.spring.batch.item.SimpleChunkProcessorInstrumentation;
+import io.opentelemetry.javaagent.instrumentation.spring.batch.item.SimpleChunkProviderInstrumentation;
import io.opentelemetry.javaagent.instrumentation.spring.batch.job.JobBuilderHelperInstrumentation;
import io.opentelemetry.javaagent.instrumentation.spring.batch.job.JobFactoryBeanInstrumentation;
import io.opentelemetry.javaagent.instrumentation.spring.batch.job.JobParserJobFactoryBeanInstrumentation;
import io.opentelemetry.javaagent.instrumentation.spring.batch.step.StepBuilderHelperInstrumentation;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
-import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -47,19 +50,20 @@ public class SpringBatchInstrumentationModule extends InstrumentationModule {
@Override
public List<TypeInstrumentation> typeInstrumentations() {
- List<TypeInstrumentation> instrumentations = new ArrayList<>();
- if (isTracingEnabled("job")) {
- instrumentations.add(new JobBuilderHelperInstrumentation());
- instrumentations.add(new JobFactoryBeanInstrumentation());
- instrumentations.add(new JobParserJobFactoryBeanInstrumentation());
- }
- if (isTracingEnabled("step")) {
- instrumentations.add(new StepBuilderHelperInstrumentation());
- }
- if (isTracingEnabled("chunk")) {
- instrumentations.add(new StepBuilderInstrumentation());
- }
- return instrumentations;
+ return Arrays.asList(
+ // job instrumentations
+ new JobBuilderHelperInstrumentation(),
+ new JobFactoryBeanInstrumentation(),
+ new JobParserJobFactoryBeanInstrumentation(),
+ // step instrumentation
+ new StepBuilderHelperInstrumentation(),
+ // chunk instrumentation
+ new StepBuilderInstrumentation(),
+ // item instrumentations
+ new ChunkOrientedTaskletInstrumentation(),
+ new SimpleChunkProviderInstrumentation(),
+ new SimpleChunkProcessorInstrumentation(),
+ new JsrChunkProcessorInstrumentation());
}
protected boolean defaultEnabled() {

ADD
instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/ChunkOrientedTaskletInstrumentation.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.batch.item;
+
+import static io.opentelemetry.javaagent.instrumentation.spring.batch.SpringBatchInstrumentationConfig.shouldTraceItems;
+import static io.opentelemetry.javaagent.instrumentation.spring.batch.item.ItemTracer.tracer;
+import static java.util.Collections.singletonMap;
+import static net.bytebuddy.matcher.ElementMatchers.isPublic;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
+import java.util.Map;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import org.springframework.batch.core.scope.context.ChunkContext;
+
+public class ChunkOrientedTaskletInstrumentation implements TypeInstrumentation {
+ @Override
+ public ElementMatcher<? super TypeDescription> typeMatcher() {
+ return named("org.springframework.batch.core.step.item.ChunkOrientedTasklet");
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ return singletonMap(
+ isPublic()
+ .and(named("execute"))
+ .and(takesArguments(2))
+ .and(takesArgument(0, named("org.springframework.batch.core.StepContribution")))
+ .and(
+ takesArgument(
+ 1, named("org.springframework.batch.core.scope.context.ChunkContext"))),
+ this.getClass().getName() + "$ExecuteAdvice");
+ }
+
+ public static class ExecuteAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void onEnter(
+ @Advice.Argument(1) ChunkContext chunkContext, @Advice.Local("otelScope") Scope scope) {
+ if (shouldTraceItems()) {
+ Context context = tracer().startChunk(chunkContext);
+ scope = context.makeCurrent();
+ }
+ }
+
+ @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
+ public static void onExit(@Advice.Local("otelScope") Scope scope) {
+ if (scope != null) {
+ scope.close();
+ }
+ }
+ }
+}

ADD
instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/ItemTracer.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.batch.item;
+
+import static io.opentelemetry.api.trace.Span.Kind.INTERNAL;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.ContextKey;
+import io.opentelemetry.instrumentation.api.tracer.BaseTracer;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.springframework.batch.core.scope.context.ChunkContext;
+
+public class ItemTracer extends BaseTracer {
+ private static final ContextKey<ChunkContext> CHUNK_CONTEXT_KEY =
+ ContextKey.named("opentelemetry-spring-batch-chunk-context-context-key");
+
+ private static final ItemTracer TRACER = new ItemTracer();
+
+ public static ItemTracer tracer() {
+ return TRACER;
+ }
+
+ /**
+ * Item-level listeners do not receive chunk/step context as parameters. Fortunately the whole
+ * chunk always executes on one thread - in Spring Batch chunk is almost synonymous with a DB
+ * transaction; this makes {@link ChunkContext} a good candidate to be stored in {@link Context}.
+ */
+ public Context startChunk(ChunkContext chunkContext) {
+ return Context.current().with(CHUNK_CONTEXT_KEY, chunkContext);
+ }
+
+ @Nullable
+ public Context startReadSpan() {
+ return startItemSpan("ItemRead");
+ }
+
+ @Nullable
+ public Context startProcessSpan() {
+ return startItemSpan("ItemProcess");
+ }
+
+ @Nullable
+ public Context startWriteSpan() {
+ return startItemSpan("ItemWrite");
+ }
+
+ @Nullable
+ private Context startItemSpan(String itemOperationName) {
+ Context currentContext = Context.current();
+
+ ChunkContext chunkContext = currentContext.get(CHUNK_CONTEXT_KEY);
+ if (chunkContext == null) {
+ return null;
+ }
+
+ String jobName = chunkContext.getStepContext().getJobName();
+ String stepName = chunkContext.getStepContext().getStepName();
+ Span span =
+ tracer
+ .spanBuilder("BatchJob " + jobName + "." + stepName + "." + itemOperationName)
+ .setSpanKind(INTERNAL)
+ .startSpan();
+
+ return currentContext.with(span);
+ }
+
+ public void end(Context context) {
+ end(Span.fromContext(context));
+ }
+
+ public void endExceptionally(Context context, Throwable throwable) {
+ endExceptionally(Span.fromContext(context), throwable);
+ }
+
+ @Override
+ protected String getInstrumentationName() {
+ return "io.opentelemetry.javaagent.spring-batch";
+ }
+}

ADD
instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/JsrChunkProcessorInstrumentation.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.batch.item;
+
+import static io.opentelemetry.javaagent.instrumentation.spring.batch.SpringBatchInstrumentationConfig.shouldTraceItems;
+import static io.opentelemetry.javaagent.instrumentation.spring.batch.item.ItemTracer.tracer;
+import static net.bytebuddy.matcher.ElementMatchers.isProtected;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
+import java.util.HashMap;
+import java.util.Map;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class JsrChunkProcessorInstrumentation implements TypeInstrumentation {
+ @Override
+ public ElementMatcher<? super TypeDescription> typeMatcher() {
+ return named("org.springframework.batch.core.jsr.step.item.JsrChunkProcessor");
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ Map<ElementMatcher<? super MethodDescription>, String> transformers = new HashMap<>();
+ transformers.put(
+ isProtected().and(named("doProvide")).and(takesArguments(2)),
+ this.getClass().getName() + "$ProvideAdvice");
+ transformers.put(
+ isProtected().and(named("doTransform")).and(takesArguments(1)),
+ this.getClass().getName() + "$TransformAdvice");
+ transformers.put(
+ isProtected().and(named("doPersist")).and(takesArguments(2)),
+ this.getClass().getName() + "$PersistAdvice");
+ return transformers;
+ }
+
+ public static class ProvideAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void onEnter(
+ @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) {
+ if (!shouldTraceItems()) {
+ return;
+ }
+ context = tracer().startReadSpan();
+ if (context != null) {
+ scope = context.makeCurrent();
+ }
+ }
+
+ @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
+ public static void onExit(
+ @Advice.Thrown Throwable thrown,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ if (scope != null) {
+ scope.close();
+ if (thrown == null) {
+ tracer().end(context);
+ } else {
+ tracer().endExceptionally(context, thrown);
+ }
+ }
+ }
+ }
+
+ public static class TransformAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void onEnter(
+ @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) {
+ if (!shouldTraceItems()) {
+ return;
+ }
+ context = tracer().startProcessSpan();
+ if (context != null) {
+ scope = context.makeCurrent();
+ }
+ }
+
+ @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
+ public static void onExit(
+ @Advice.Thrown Throwable thrown,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ if (scope != null) {
+ scope.close();
+ if (thrown == null) {
+ tracer().end(context);
+ } else {
+ tracer().endExceptionally(context, thrown);
+ }
+ }
+ }
+ }
+
+ public static class PersistAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void onEnter(
+ @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) {
+ if (!shouldTraceItems()) {
+ return;
+ }
+ context = tracer().startWriteSpan();
+ if (context != null) {
+ scope = context.makeCurrent();
+ }
+ }
+
+ @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
+ public static void onExit(
+ @Advice.Thrown Throwable thrown,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ if (scope != null) {
+ scope.close();
+ if (thrown == null) {
+ tracer().end(context);
+ } else {
+ tracer().endExceptionally(context, thrown);
+ }
+ }
+ }
+ }
+}

ADD
instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/SimpleChunkProcessorInstrumentation.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.batch.item;
+
+import static io.opentelemetry.javaagent.instrumentation.spring.batch.SpringBatchInstrumentationConfig.shouldTraceItems;
+import static io.opentelemetry.javaagent.instrumentation.spring.batch.item.ItemTracer.tracer;
+import static net.bytebuddy.matcher.ElementMatchers.isProtected;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
+import java.util.HashMap;
+import java.util.Map;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import org.springframework.batch.item.ItemProcessor;
+import org.springframework.batch.item.ItemWriter;
+
+public class SimpleChunkProcessorInstrumentation implements TypeInstrumentation {
+ @Override
+ public ElementMatcher<? super TypeDescription> typeMatcher() {
+ return named("org.springframework.batch.core.step.item.SimpleChunkProcessor");
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ Map<ElementMatcher<? super MethodDescription>, String> transformers = new HashMap<>();
+ transformers.put(
+ isProtected().and(named("doProcess")).and(takesArguments(1)),
+ this.getClass().getName() + "$ProcessAdvice");
+ transformers.put(
+ isProtected().and(named("doWrite")).and(takesArguments(1)),
+ this.getClass().getName() + "$WriteAdvice");
+ return transformers;
+ }
+
+ public static class ProcessAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void onEnter(
+ @Advice.FieldValue("itemProcessor") ItemProcessor<?, ?> itemProcessor,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ if (!shouldTraceItems()) {
+ return;
+ }
+ if (itemProcessor == null) {
+ return;
+ }
+ context = tracer().startProcessSpan();
+ if (context != null) {
+ scope = context.makeCurrent();
+ }
+ }
+
+ @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
+ public static void onExit(
+ @Advice.Thrown Throwable thrown,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ if (scope != null) {
+ scope.close();
+ if (thrown == null) {
+ tracer().end(context);
+ } else {
+ tracer().endExceptionally(context, thrown);
+ }
+ }
+ }
+ }
+
+ public static class WriteAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void onEnter(
+ @Advice.FieldValue("itemWriter") ItemWriter<?> itemWriter,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ if (!shouldTraceItems()) {
+ return;
+ }
+ if (itemWriter == null) {
+ return;
+ }
+ context = tracer().startWriteSpan();
+ if (context != null) {
+ scope = context.makeCurrent();
+ }
+ }
+
+ @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
+ public static void onExit(
+ @Advice.Thrown Throwable thrown,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ if (scope != null) {
+ scope.close();
+ if (thrown == null) {
+ tracer().end(context);
+ } else {
+ tracer().endExceptionally(context, thrown);
+ }
+ }
+ }
+ }
+}

ADD
instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/SimpleChunkProviderInstrumentation.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.spring.batch.item;
+
+import static io.opentelemetry.javaagent.instrumentation.spring.batch.SpringBatchInstrumentationConfig.shouldTraceItems;
+import static io.opentelemetry.javaagent.instrumentation.spring.batch.item.ItemTracer.tracer;
+import static java.util.Collections.singletonMap;
+import static net.bytebuddy.matcher.ElementMatchers.isProtected;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
+import java.util.Map;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+// item read instrumentation *cannot* use ItemReadListener: sometimes afterRead() is not called
+// after beforeRead(), using listener here would cause unfinished spans/scopes
+public class SimpleChunkProviderInstrumentation implements TypeInstrumentation {
+ @Override
+ public ElementMatcher<? super TypeDescription> typeMatcher() {
+ return named("org.springframework.batch.core.step.item.SimpleChunkProvider");
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ return singletonMap(
+ isProtected().and(named("doRead")).and(takesArguments(0)),
+ this.getClass().getName() + "$ReadAdvice");
+ }
+
+ public static class ReadAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void onEnter(
+ @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) {
+ if (!shouldTraceItems()) {
+ return;
+ }
+ context = tracer().startReadSpan();
+ if (context != null) {
+ scope = context.makeCurrent();
+ }
+ }
+
+ @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
+ public static void onExit(
+ @Advice.Thrown Throwable thrown,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ if (scope != null) {
+ scope.close();
+ if (thrown == null) {
+ tracer().end(context);
+ } else {
+ tracer().endExceptionally(context, thrown);
+ }
+ }
+ }
+ }
+}

ADD
instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/ItemLevelSpanTest.groovy
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import static io.opentelemetry.api.trace.Span.Kind.INTERNAL
+import static java.util.Collections.emptyMap
+
+import io.opentelemetry.instrumentation.test.AgentTestRunner
+import org.springframework.batch.core.JobParameter
+import org.springframework.context.ConfigurableApplicationContext
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import org.springframework.context.support.ClassPathXmlApplicationContext
+
+abstract class ItemLevelSpanTest extends AgentTestRunner {
+ abstract runJob(String jobName, Map<String, JobParameter> params = emptyMap())
+
+ def "should trace item read, process and write calls"() {
+ when:
+ runJob("itemsAndTaskletJob")
+
+ then:
+ assertTraces(1) {
+ trace(0, 37) {
+ span(0) {
+ name "BatchJob itemsAndTaskletJob"
+ kind INTERNAL
+ }
+
+ // item step
+ span(1) {
+ name "BatchJob itemsAndTaskletJob.itemStep"
+ kind INTERNAL
+ childOf span(0)
+ }
+
+ // chunk 1, items 0-5
+ span(2) {
+ name "BatchJob itemsAndTaskletJob.itemStep.Chunk"
+ kind INTERNAL
+ childOf span(1)
+ }
+ (3..7).forEach {
+ span(it) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemRead"
+ kind INTERNAL
+ childOf span(2)
+ }
+ }
+ (8..12).forEach {
+ span(it) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemProcess"
+ kind INTERNAL
+ childOf span(2)
+ }
+ }
+ span(13) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemWrite"
+ kind INTERNAL
+ childOf span(2)
+ }
+
+ // chunk 2, items 5-10
+ span(14) {
+ name "BatchJob itemsAndTaskletJob.itemStep.Chunk"
+ kind INTERNAL
+ childOf span(1)
+ }
+ (15..19).forEach {
+ span(it) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemRead"
+ kind INTERNAL
+ childOf span(14)
+ }
+ }
+ (20..24).forEach {
+ span(it) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemProcess"
+ kind INTERNAL
+ childOf span(14)
+ }
+ }
+ span(25) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemWrite"
+ kind INTERNAL
+ childOf span(14)
+ }
+
+ // chunk 3, items 10-13
+ span(26) {
+ name "BatchJob itemsAndTaskletJob.itemStep.Chunk"
+ kind INTERNAL
+ childOf span(1)
+ }
+ // +1 for last read returning end of stream marker
+ (27..30).forEach {
+ span(it) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemRead"
+ kind INTERNAL
+ childOf span(26)
+ }
+ }
+ (31..33).forEach {
+ span(it) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemProcess"
+ kind INTERNAL
+ childOf span(26)
+ }
+ }
+ span(34) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemWrite"
+ kind INTERNAL
+ childOf span(26)
+ }
+
+ // tasklet step
+ span(35) {
+ name "BatchJob itemsAndTaskletJob.taskletStep"
+ kind INTERNAL
+ childOf span(0)
+ }
+ span(36) {
+ name "BatchJob itemsAndTaskletJob.taskletStep.Chunk"
+ kind INTERNAL
+ childOf span(35)
+ }
+ }
+ }
+ }
+}
+
+class JavaConfigItemLevelSpanTest extends ItemLevelSpanTest implements ApplicationConfigTrait {
+ @Override
+ ConfigurableApplicationContext createApplicationContext() {
+ new AnnotationConfigApplicationContext(SpringBatchApplication)
+ }
+}
+
+class XmlConfigItemLevelSpanTest extends ItemLevelSpanTest implements ApplicationConfigTrait {
+ @Override
+ ConfigurableApplicationContext createApplicationContext() {
+ new ClassPathXmlApplicationContext("spring-batch.xml")
+ }
+}
+
+// JsrChunkProcessor works a bit differently than the "standard" one and does not read the whole
+// chunk at once, it reads every item separately; it results in a different span ordering, that's
+// why it has a completely separate test class
+class JsrConfigItemLevelSpanTest extends AgentTestRunner implements JavaxBatchConfigTrait {
+ def "should trace item read, process and write calls"() {
+ when:
+ runJob("itemsAndTaskletJob", [:])
+
+ then:
+ assertTraces(1) {
+ trace(0, 37) {
+ span(0) {
+ name "BatchJob itemsAndTaskletJob"
+ kind INTERNAL
+ }
+
+ // item step
+ span(1) {
+ name "BatchJob itemsAndTaskletJob.itemStep"
+ kind INTERNAL
+ childOf span(0)
+ }
+
+ // chunk 1, items 0-5
+ span(2) {
+ name "BatchJob itemsAndTaskletJob.itemStep.Chunk"
+ kind INTERNAL
+ childOf span(1)
+ }
+ (3..11).step(2) {
+ println it
+ span(it) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemRead"
+ kind INTERNAL
+ childOf span(2)
+ }
+ span(it + 1) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemProcess"
+ kind INTERNAL
+ childOf span(2)
+ }
+ }
+ span(13) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemWrite"
+ kind INTERNAL
+ childOf span(2)
+ }
+
+ // chunk 2, items 5-10
+ span(14) {
+ name "BatchJob itemsAndTaskletJob.itemStep.Chunk"
+ kind INTERNAL
+ childOf span(1)
+ }
+ (15..23).step(2) {
+ println it
+ span(it) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemRead"
+ kind INTERNAL
+ childOf span(14)
+ }
+ span(it + 1) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemProcess"
+ kind INTERNAL
+ childOf span(14)
+ }
+ }
+ span(25) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemWrite"
+ kind INTERNAL
+ childOf span(14)
+ }
+
+ // chunk 3, items 10-13
+ span(26) {
+ name "BatchJob itemsAndTaskletJob.itemStep.Chunk"
+ kind INTERNAL
+ childOf span(1)
+ }
+ (27..32).step(2) {
+ println it
+ span(it) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemRead"
+ kind INTERNAL
+ childOf span(26)
+ }
+ span(it + 1) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemProcess"
+ kind INTERNAL
+ childOf span(26)
+ }
+ }
+ // last read returning end of stream marker
+ span(33) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemRead"
+ kind INTERNAL
+ childOf span(26)
+ }
+ span(34) {
+ name "BatchJob itemsAndTaskletJob.itemStep.ItemWrite"
+ kind INTERNAL
+ childOf span(26)
+ }
+
+ // tasklet step
+ span(35) {
+ name "BatchJob itemsAndTaskletJob.taskletStep"
+ kind INTERNAL
+ childOf span(0)
+ }
+ span(36) {
+ name "BatchJob itemsAndTaskletJob.taskletStep.Chunk"
+ kind INTERNAL
+ childOf span(35)
+ }
+ }
+ }
+ }
+}

Instrument spring-batch: item-level spans (#2047)