From 09de4bd1059185f20b2ec2da5c2f34d960e06b0f Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Wed, 4 Sep 2024 20:52:09 +0200 Subject: [PATCH] Added SpanProcessor OnEnding callback (#6367) Co-authored-by: jack-berg <34418638+jack-berg@users.noreply.github.com> --- .../sdk/trace/MultiSpanProcessor.java | 25 ++++- .../io/opentelemetry/sdk/trace/SdkSpan.java | 66 +++++++++---- .../trace/internal/ExtendedSpanProcessor.java | 41 ++++++++ .../sdk/trace/MultiSpanProcessorTest.java | 27 ++++-- .../opentelemetry/sdk/trace/SdkSpanTest.java | 94 ++++++++++++++++++- .../ExtendedSpanProcessorUsageTest.java | 85 +++++++++++++++++ 6 files changed, 313 insertions(+), 25 deletions(-) create mode 100644 sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/ExtendedSpanProcessor.java create mode 100644 sdk/trace/src/test/java/io/opentelemetry/sdk/trace/internal/ExtendedSpanProcessorUsageTest.java diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/MultiSpanProcessor.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/MultiSpanProcessor.java index a06b4b69579..65b37096568 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/MultiSpanProcessor.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/MultiSpanProcessor.java @@ -7,6 +7,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.internal.ExtendedSpanProcessor; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -16,8 +17,9 @@ * Implementation of the {@code SpanProcessor} that simply forwards all received events to a list of * {@code SpanProcessor}s. */ -final class MultiSpanProcessor implements SpanProcessor { +final class MultiSpanProcessor implements ExtendedSpanProcessor { private final List spanProcessorsStart; + private final List spanProcessorsEnding; private final List spanProcessorsEnd; private final List spanProcessorsAll; private final AtomicBoolean isShutdown = new AtomicBoolean(false); @@ -58,6 +60,18 @@ public boolean isEndRequired() { return !spanProcessorsEnd.isEmpty(); } + @Override + public void onEnding(ReadWriteSpan span) { + for (ExtendedSpanProcessor spanProcessor : spanProcessorsEnding) { + spanProcessor.onEnding(span); + } + } + + @Override + public boolean isOnEndingRequired() { + return !spanProcessorsEnding.isEmpty(); + } + @Override public CompletableResultCode shutdown() { if (isShutdown.getAndSet(true)) { @@ -83,10 +97,17 @@ private MultiSpanProcessor(List spanProcessors) { this.spanProcessorsAll = spanProcessors; this.spanProcessorsStart = new ArrayList<>(spanProcessorsAll.size()); this.spanProcessorsEnd = new ArrayList<>(spanProcessorsAll.size()); + this.spanProcessorsEnding = new ArrayList<>(spanProcessorsAll.size()); for (SpanProcessor spanProcessor : spanProcessorsAll) { if (spanProcessor.isStartRequired()) { spanProcessorsStart.add(spanProcessor); } + if (spanProcessor instanceof ExtendedSpanProcessor) { + ExtendedSpanProcessor extendedSpanProcessor = (ExtendedSpanProcessor) spanProcessor; + if (extendedSpanProcessor.isOnEndingRequired()) { + spanProcessorsEnding.add(extendedSpanProcessor); + } + } if (spanProcessor.isEndRequired()) { spanProcessorsEnd.add(spanProcessor); } @@ -98,6 +119,8 @@ public String toString() { return "MultiSpanProcessor{" + "spanProcessorsStart=" + spanProcessorsStart + + ", spanProcessorsEnding=" + + spanProcessorsEnding + ", spanProcessorsEnd=" + spanProcessorsEnd + ", spanProcessorsAll=" diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java index 5011b5a3d7d..1580b05c465 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java @@ -23,6 +23,7 @@ import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.sdk.trace.internal.ExtendedSpanProcessor; import io.opentelemetry.sdk.trace.internal.data.ExceptionEventData; import java.util.ArrayList; import java.util.Collections; @@ -95,9 +96,24 @@ final class SdkSpan implements ReadWriteSpan { @GuardedBy("lock") private long endEpochNanos; - // True if the span is ended. + private enum EndState { + NOT_ENDED, + ENDING, + ENDED + } + @GuardedBy("lock") - private boolean hasEnded; + private EndState hasEnded; + + /** + * The thread on which {@link #end()} is called and which will be invoking the {@link + * SpanProcessor}s. This field is used to ensure that only this thread may modify the span while + * it is in state {@link EndState#ENDING} to prevent concurrent updates outside of {@link + * ExtendedSpanProcessor#onEnding(ReadWriteSpan)}. + */ + @GuardedBy("lock") + @Nullable + private Thread spanEndingThread; private SdkSpan( SpanContext context, @@ -122,7 +138,7 @@ private SdkSpan( this.kind = kind; this.spanProcessor = spanProcessor; this.resource = resource; - this.hasEnded = false; + this.hasEnded = EndState.NOT_ENDED; this.clock = clock; this.startEpochNanos = startEpochNanos; this.attributes = attributes; @@ -220,7 +236,7 @@ public SpanData toSpanData() { status, name, endEpochNanos, - hasEnded); + hasEnded == EndState.ENDED); } } @@ -242,7 +258,7 @@ public Attributes getAttributes() { @Override public boolean hasEnded() { synchronized (lock) { - return hasEnded; + return hasEnded == EndState.ENDED; } } @@ -288,7 +304,7 @@ public InstrumentationScopeInfo getInstrumentationScopeInfo() { @Override public long getLatencyNanos() { synchronized (lock) { - return (hasEnded ? endEpochNanos : clock.now()) - startEpochNanos; + return (hasEnded == EndState.NOT_ENDED ? clock.now() : endEpochNanos) - startEpochNanos; } } @@ -303,7 +319,7 @@ public ReadWriteSpan setAttribute(AttributeKey key, T value) { return this; } synchronized (lock) { - if (hasEnded) { + if (!isModifiableByCurrentThread()) { logger.log(Level.FINE, "Calling setAttribute() on an ended Span."); return this; } @@ -318,6 +334,12 @@ public ReadWriteSpan setAttribute(AttributeKey key, T value) { return this; } + @GuardedBy("lock") + private boolean isModifiableByCurrentThread() { + return hasEnded == EndState.NOT_ENDED + || (hasEnded == EndState.ENDING && Thread.currentThread() == spanEndingThread); + } + @Override public ReadWriteSpan addEvent(String name) { if (name == null) { @@ -380,7 +402,7 @@ public ReadWriteSpan addEvent(String name, Attributes attributes, long timestamp private void addTimedEvent(EventData timedEvent) { synchronized (lock) { - if (hasEnded) { + if (!isModifiableByCurrentThread()) { logger.log(Level.FINE, "Calling addEvent() on an ended Span."); return; } @@ -400,7 +422,7 @@ public ReadWriteSpan setStatus(StatusCode statusCode, @Nullable String descripti return this; } synchronized (lock) { - if (hasEnded) { + if (!isModifiableByCurrentThread()) { logger.log(Level.FINE, "Calling setStatus() on an ended Span."); return this; } else if (this.status.getStatusCode() == StatusCode.OK) { @@ -438,7 +460,7 @@ public ReadWriteSpan updateName(String name) { return this; } synchronized (lock) { - if (hasEnded) { + if (!isModifiableByCurrentThread()) { logger.log(Level.FINE, "Calling updateName() on an ended Span."); return this; } @@ -463,7 +485,7 @@ public Span addLink(SpanContext spanContext, Attributes attributes) { spanLimits.getMaxNumberOfAttributesPerLink(), spanLimits.getMaxAttributeValueLength())); synchronized (lock) { - if (hasEnded) { + if (!isModifiableByCurrentThread()) { logger.log(Level.FINE, "Calling addLink() on an ended Span."); return this; } @@ -493,12 +515,22 @@ public void end(long timestamp, TimeUnit unit) { private void endInternal(long endEpochNanos) { synchronized (lock) { - if (hasEnded) { - logger.log(Level.FINE, "Calling end() on an ended Span."); + if (hasEnded != EndState.NOT_ENDED) { + logger.log(Level.FINE, "Calling end() on an ended or ending Span."); return; } this.endEpochNanos = endEpochNanos; - hasEnded = true; + spanEndingThread = Thread.currentThread(); + hasEnded = EndState.ENDING; + } + if (spanProcessor instanceof ExtendedSpanProcessor) { + ExtendedSpanProcessor extendedSpanProcessor = (ExtendedSpanProcessor) spanProcessor; + if (extendedSpanProcessor.isOnEndingRequired()) { + extendedSpanProcessor.onEnding(this); + } + } + synchronized (lock) { + hasEnded = EndState.ENDED; } if (spanProcessor.isEndRequired()) { spanProcessor.onEnd(this); @@ -508,7 +540,7 @@ private void endInternal(long endEpochNanos) { @Override public boolean isRecording() { synchronized (lock) { - return !hasEnded; + return hasEnded != EndState.ENDED; } } @@ -533,7 +565,7 @@ private List getImmutableTimedEvents() { // if the span has ended, then the events are unmodifiable // so we can return them directly and save copying all the data. - if (hasEnded) { + if (hasEnded == EndState.ENDED) { return Collections.unmodifiableList(events); } @@ -547,7 +579,7 @@ private Attributes getImmutableAttributes() { } // if the span has ended, then the attributes are unmodifiable, // so we can return them directly and save copying all the data. - if (hasEnded) { + if (hasEnded == EndState.ENDED) { return attributes; } // otherwise, make a copy of the data into an immutable container. diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/ExtendedSpanProcessor.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/ExtendedSpanProcessor.java new file mode 100644 index 00000000000..5c608bf8275 --- /dev/null +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/ExtendedSpanProcessor.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.internal; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; + +/** + * Extended {@link SpanProcessor} with experimental APIs. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public interface ExtendedSpanProcessor extends SpanProcessor { + + /** + * Called when a {@link io.opentelemetry.api.trace.Span} is ended, but before {@link + * SpanProcessor#onEnd(ReadableSpan)} is invoked with an immutable variant of this span. This + * means that the span will still be mutable. Note that the span will only be modifiable + * synchronously from this callback, concurrent modifications from other threads will be + * prevented. Only called if {@link Span#isRecording()} returns true. + * + *

This method is called synchronously on the execution thread, should not throw or block the + * execution thread. + * + * @param span the {@code Span} that is just about to be ended. + */ + void onEnding(ReadWriteSpan span); + + /** + * Returns {@code true} if this {@link SpanProcessor} requires onEnding events. + * + * @return {@code true} if this {@link SpanProcessor} requires onEnding events. + */ + boolean isOnEndingRequired(); +} diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/MultiSpanProcessorTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/MultiSpanProcessorTest.java index c51fe61c954..34ac6e1c03d 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/MultiSpanProcessorTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/MultiSpanProcessorTest.java @@ -14,6 +14,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.internal.ExtendedSpanProcessor; import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; @@ -27,18 +28,20 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class MultiSpanProcessorTest { - @Mock private SpanProcessor spanProcessor1; - @Mock private SpanProcessor spanProcessor2; + @Mock private ExtendedSpanProcessor spanProcessor1; + @Mock private ExtendedSpanProcessor spanProcessor2; @Mock private ReadableSpan readableSpan; @Mock private ReadWriteSpan readWriteSpan; @BeforeEach void setUp() { when(spanProcessor1.isStartRequired()).thenReturn(true); + when(spanProcessor1.isOnEndingRequired()).thenReturn(true); when(spanProcessor1.isEndRequired()).thenReturn(true); when(spanProcessor1.forceFlush()).thenReturn(CompletableResultCode.ofSuccess()); when(spanProcessor1.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); when(spanProcessor2.isStartRequired()).thenReturn(true); + when(spanProcessor2.isOnEndingRequired()).thenReturn(true); when(spanProcessor2.isEndRequired()).thenReturn(true); when(spanProcessor2.forceFlush()).thenReturn(CompletableResultCode.ofSuccess()); when(spanProcessor2.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); @@ -61,12 +64,17 @@ void oneSpanProcessor() { @Test void twoSpanProcessor() { - SpanProcessor multiSpanProcessor = - SpanProcessor.composite(Arrays.asList(spanProcessor1, spanProcessor2)); + ExtendedSpanProcessor multiSpanProcessor = + (ExtendedSpanProcessor) + SpanProcessor.composite(Arrays.asList(spanProcessor1, spanProcessor2)); multiSpanProcessor.onStart(Context.root(), readWriteSpan); verify(spanProcessor1).onStart(same(Context.root()), same(readWriteSpan)); verify(spanProcessor2).onStart(same(Context.root()), same(readWriteSpan)); + multiSpanProcessor.onEnding(readWriteSpan); + verify(spanProcessor1).onEnding(same(readWriteSpan)); + verify(spanProcessor2).onEnding(same(readWriteSpan)); + multiSpanProcessor.onEnd(readableSpan); verify(spanProcessor1).onEnd(same(readableSpan)); verify(spanProcessor2).onEnd(same(readableSpan)); @@ -83,9 +91,11 @@ void twoSpanProcessor() { @Test void twoSpanProcessor_DifferentRequirements() { when(spanProcessor1.isEndRequired()).thenReturn(false); + when(spanProcessor2.isOnEndingRequired()).thenReturn(false); when(spanProcessor2.isStartRequired()).thenReturn(false); - SpanProcessor multiSpanProcessor = - SpanProcessor.composite(Arrays.asList(spanProcessor1, spanProcessor2)); + ExtendedSpanProcessor multiSpanProcessor = + (ExtendedSpanProcessor) + SpanProcessor.composite(Arrays.asList(spanProcessor1, spanProcessor2)); assertThat(multiSpanProcessor.isStartRequired()).isTrue(); assertThat(multiSpanProcessor.isEndRequired()).isTrue(); @@ -94,6 +104,10 @@ void twoSpanProcessor_DifferentRequirements() { verify(spanProcessor1).onStart(same(Context.root()), same(readWriteSpan)); verify(spanProcessor2, times(0)).onStart(any(Context.class), any(ReadWriteSpan.class)); + multiSpanProcessor.onEnding(readWriteSpan); + verify(spanProcessor1).onEnding(same(readWriteSpan)); + verify(spanProcessor2, times(0)).onEnding(any(ReadWriteSpan.class)); + multiSpanProcessor.onEnd(readableSpan); verify(spanProcessor1, times(0)).onEnd(any(ReadableSpan.class)); verify(spanProcessor2).onEnd(same(readableSpan)); @@ -117,6 +131,7 @@ void stringRepresentation() { .hasToString( "MultiSpanProcessor{" + "spanProcessorsStart=[spanProcessor1, spanProcessor1], " + + "spanProcessorsEnding=[spanProcessor1, spanProcessor1], " + "spanProcessorsEnd=[spanProcessor1, spanProcessor1], " + "spanProcessorsAll=[spanProcessor1, spanProcessor1]}"); } diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java index df56d05a07e..7814074269b 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java @@ -18,6 +18,8 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -42,6 +44,7 @@ import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.sdk.trace.internal.ExtendedSpanProcessor; import io.opentelemetry.sdk.trace.internal.data.ExceptionEventData; import java.io.PrintWriter; import java.io.StringWriter; @@ -58,6 +61,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.IntStream; import javax.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; @@ -90,7 +95,7 @@ class SdkSpanTest { private final Map attributes = new HashMap<>(); private Attributes expectedAttributes; private final LinkData link = LinkData.create(spanContext); - @Mock private SpanProcessor spanProcessor; + @Mock private ExtendedSpanProcessor spanProcessor; private TestClock testClock; @@ -107,6 +112,7 @@ void setUp() { expectedAttributes = builder.build(); testClock = TestClock.create(Instant.ofEpochSecond(0, START_EPOCH_NANOS)); when(spanProcessor.isStartRequired()).thenReturn(true); + when(spanProcessor.isOnEndingRequired()).thenReturn(true); when(spanProcessor.isEndRequired()).thenReturn(true); } @@ -140,6 +146,92 @@ void endSpanTwice_DoNotCrash() { assertThat(span.hasEnded()).isTrue(); } + @Test + void onEnding_spanStillMutable() { + SdkSpan span = createTestSpan(SpanKind.INTERNAL); + + AttributeKey dummyAttrib = AttributeKey.stringKey("processor_foo"); + + AtomicBoolean endedStateInProcessor = new AtomicBoolean(); + doAnswer( + invocation -> { + ReadWriteSpan sp = invocation.getArgument(0, ReadWriteSpan.class); + assertThat(sp.hasEnded()).isFalse(); + sp.end(); // should have no effect, nested end should be detected + endedStateInProcessor.set(sp.hasEnded()); + sp.setAttribute(dummyAttrib, "bar"); + return null; + }) + .when(spanProcessor) + .onEnding(any()); + + span.end(); + verify(spanProcessor).onEnding(same(span)); + assertThat(span.hasEnded()).isTrue(); + assertThat(endedStateInProcessor.get()).isFalse(); + assertThat(span.getAttribute(dummyAttrib)).isEqualTo("bar"); + } + + @Test + void onEnding_concurrentModificationsPrevented() { + SdkSpan span = createTestSpan(SpanKind.INTERNAL); + + AttributeKey syncAttrib = AttributeKey.stringKey("sync_foo"); + AttributeKey concurrentAttrib = AttributeKey.stringKey("concurrent_foo"); + + doAnswer( + invocation -> { + ReadWriteSpan sp = invocation.getArgument(0, ReadWriteSpan.class); + + Thread concurrent = + new Thread( + () -> { + sp.setAttribute(concurrentAttrib, "concurrent_bar"); + }); + concurrent.start(); + concurrent.join(); + + sp.setAttribute(syncAttrib, "sync_bar"); + + return null; + }) + .when(spanProcessor) + .onEnding(any()); + + span.end(); + verify(spanProcessor).onEnding(same(span)); + assertThat(span.getAttribute(concurrentAttrib)).isNull(); + assertThat(span.getAttribute(syncAttrib)).isEqualTo("sync_bar"); + } + + @Test + void onEnding_latencyPinned() { + SdkSpan span = createTestSpan(SpanKind.INTERNAL); + + AtomicLong spanLatencyInProcessor = new AtomicLong(); + doAnswer( + invocation -> { + ReadWriteSpan sp = invocation.getArgument(0, ReadWriteSpan.class); + + testClock.advance(Duration.ofSeconds(100)); + spanLatencyInProcessor.set(sp.getLatencyNanos()); + return null; + }) + .when(spanProcessor) + .onEnding(any()); + + testClock.advance(Duration.ofSeconds(1)); + long expectedDuration = testClock.now() - START_EPOCH_NANOS; + + assertThat(span.getLatencyNanos()).isEqualTo(expectedDuration); + + span.end(); + verify(spanProcessor).onEnding(same(span)); + assertThat(span.hasEnded()).isTrue(); + assertThat(span.getLatencyNanos()).isEqualTo(expectedDuration); + assertThat(spanLatencyInProcessor.get()).isEqualTo(expectedDuration); + } + @Test void toSpanData_ActiveSpan() { SdkSpan span = createTestSpan(SpanKind.INTERNAL); diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/internal/ExtendedSpanProcessorUsageTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/internal/ExtendedSpanProcessorUsageTest.java new file mode 100644 index 00000000000..3185aa9a1df --- /dev/null +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/internal/ExtendedSpanProcessorUsageTest.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.internal; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import org.junit.jupiter.api.Test; + +/** Demonstrating usage of {@link ExtendedSpanProcessor}. */ +class ExtendedSpanProcessorUsageTest { + + private static final AttributeKey FOO_KEY = AttributeKey.stringKey("foo"); + private static final AttributeKey BAR_KEY = AttributeKey.stringKey("bar"); + + private static class CopyFooToBarProcessor implements ExtendedSpanProcessor { + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) {} + + @Override + public boolean isStartRequired() { + return false; + } + + @Override + public void onEnd(ReadableSpan span) {} + + @Override + public boolean isEndRequired() { + return false; + } + + @Override + public void onEnding(ReadWriteSpan span) { + String val = span.getAttribute(FOO_KEY); + span.setAttribute(BAR_KEY, val); + } + + @Override + public boolean isOnEndingRequired() { + return true; + } + } + + @Test + void extendedSpanProcessorUsage() { + InMemorySpanExporter exporter = InMemorySpanExporter.create(); + + try (SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(exporter)) + .addSpanProcessor(new CopyFooToBarProcessor()) + .build()) { + + Tracer tracer = tracerProvider.get("dummy-tracer"); + Span span = tracer.spanBuilder("my-span").startSpan(); + + span.setAttribute(FOO_KEY, "Hello!"); + + span.end(); + + assertThat(exporter.getFinishedSpanItems()) + .hasSize(1) + .first() + .satisfies( + spanData -> { + assertThat(spanData.getAttributes()) + .containsEntry(FOO_KEY, "Hello!") + .containsEntry(BAR_KEY, "Hello!"); + }); + } + } +}