Skip to content

Commit 09de4bd

Browse files
JonasKunzjack-berg
andauthored
Added SpanProcessor OnEnding callback (#6367)
Co-authored-by: jack-berg <[email protected]>
1 parent bc2fad4 commit 09de4bd

File tree

6 files changed

+313
-25
lines changed

6 files changed

+313
-25
lines changed

sdk/trace/src/main/java/io/opentelemetry/sdk/trace/MultiSpanProcessor.java

+24-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import io.opentelemetry.context.Context;
99
import io.opentelemetry.sdk.common.CompletableResultCode;
10+
import io.opentelemetry.sdk.trace.internal.ExtendedSpanProcessor;
1011
import java.util.ArrayList;
1112
import java.util.List;
1213
import java.util.Objects;
@@ -16,8 +17,9 @@
1617
* Implementation of the {@code SpanProcessor} that simply forwards all received events to a list of
1718
* {@code SpanProcessor}s.
1819
*/
19-
final class MultiSpanProcessor implements SpanProcessor {
20+
final class MultiSpanProcessor implements ExtendedSpanProcessor {
2021
private final List<SpanProcessor> spanProcessorsStart;
22+
private final List<ExtendedSpanProcessor> spanProcessorsEnding;
2123
private final List<SpanProcessor> spanProcessorsEnd;
2224
private final List<SpanProcessor> spanProcessorsAll;
2325
private final AtomicBoolean isShutdown = new AtomicBoolean(false);
@@ -58,6 +60,18 @@ public boolean isEndRequired() {
5860
return !spanProcessorsEnd.isEmpty();
5961
}
6062

63+
@Override
64+
public void onEnding(ReadWriteSpan span) {
65+
for (ExtendedSpanProcessor spanProcessor : spanProcessorsEnding) {
66+
spanProcessor.onEnding(span);
67+
}
68+
}
69+
70+
@Override
71+
public boolean isOnEndingRequired() {
72+
return !spanProcessorsEnding.isEmpty();
73+
}
74+
6175
@Override
6276
public CompletableResultCode shutdown() {
6377
if (isShutdown.getAndSet(true)) {
@@ -83,10 +97,17 @@ private MultiSpanProcessor(List<SpanProcessor> spanProcessors) {
8397
this.spanProcessorsAll = spanProcessors;
8498
this.spanProcessorsStart = new ArrayList<>(spanProcessorsAll.size());
8599
this.spanProcessorsEnd = new ArrayList<>(spanProcessorsAll.size());
100+
this.spanProcessorsEnding = new ArrayList<>(spanProcessorsAll.size());
86101
for (SpanProcessor spanProcessor : spanProcessorsAll) {
87102
if (spanProcessor.isStartRequired()) {
88103
spanProcessorsStart.add(spanProcessor);
89104
}
105+
if (spanProcessor instanceof ExtendedSpanProcessor) {
106+
ExtendedSpanProcessor extendedSpanProcessor = (ExtendedSpanProcessor) spanProcessor;
107+
if (extendedSpanProcessor.isOnEndingRequired()) {
108+
spanProcessorsEnding.add(extendedSpanProcessor);
109+
}
110+
}
90111
if (spanProcessor.isEndRequired()) {
91112
spanProcessorsEnd.add(spanProcessor);
92113
}
@@ -98,6 +119,8 @@ public String toString() {
98119
return "MultiSpanProcessor{"
99120
+ "spanProcessorsStart="
100121
+ spanProcessorsStart
122+
+ ", spanProcessorsEnding="
123+
+ spanProcessorsEnding
101124
+ ", spanProcessorsEnd="
102125
+ spanProcessorsEnd
103126
+ ", spanProcessorsAll="

sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java

+49-17
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.opentelemetry.sdk.trace.data.LinkData;
2424
import io.opentelemetry.sdk.trace.data.SpanData;
2525
import io.opentelemetry.sdk.trace.data.StatusData;
26+
import io.opentelemetry.sdk.trace.internal.ExtendedSpanProcessor;
2627
import io.opentelemetry.sdk.trace.internal.data.ExceptionEventData;
2728
import java.util.ArrayList;
2829
import java.util.Collections;
@@ -95,9 +96,24 @@ final class SdkSpan implements ReadWriteSpan {
9596
@GuardedBy("lock")
9697
private long endEpochNanos;
9798

98-
// True if the span is ended.
99+
private enum EndState {
100+
NOT_ENDED,
101+
ENDING,
102+
ENDED
103+
}
104+
99105
@GuardedBy("lock")
100-
private boolean hasEnded;
106+
private EndState hasEnded;
107+
108+
/**
109+
* The thread on which {@link #end()} is called and which will be invoking the {@link
110+
* SpanProcessor}s. This field is used to ensure that only this thread may modify the span while
111+
* it is in state {@link EndState#ENDING} to prevent concurrent updates outside of {@link
112+
* ExtendedSpanProcessor#onEnding(ReadWriteSpan)}.
113+
*/
114+
@GuardedBy("lock")
115+
@Nullable
116+
private Thread spanEndingThread;
101117

102118
private SdkSpan(
103119
SpanContext context,
@@ -122,7 +138,7 @@ private SdkSpan(
122138
this.kind = kind;
123139
this.spanProcessor = spanProcessor;
124140
this.resource = resource;
125-
this.hasEnded = false;
141+
this.hasEnded = EndState.NOT_ENDED;
126142
this.clock = clock;
127143
this.startEpochNanos = startEpochNanos;
128144
this.attributes = attributes;
@@ -220,7 +236,7 @@ public SpanData toSpanData() {
220236
status,
221237
name,
222238
endEpochNanos,
223-
hasEnded);
239+
hasEnded == EndState.ENDED);
224240
}
225241
}
226242

@@ -242,7 +258,7 @@ public Attributes getAttributes() {
242258
@Override
243259
public boolean hasEnded() {
244260
synchronized (lock) {
245-
return hasEnded;
261+
return hasEnded == EndState.ENDED;
246262
}
247263
}
248264

@@ -288,7 +304,7 @@ public InstrumentationScopeInfo getInstrumentationScopeInfo() {
288304
@Override
289305
public long getLatencyNanos() {
290306
synchronized (lock) {
291-
return (hasEnded ? endEpochNanos : clock.now()) - startEpochNanos;
307+
return (hasEnded == EndState.NOT_ENDED ? clock.now() : endEpochNanos) - startEpochNanos;
292308
}
293309
}
294310

@@ -303,7 +319,7 @@ public <T> ReadWriteSpan setAttribute(AttributeKey<T> key, T value) {
303319
return this;
304320
}
305321
synchronized (lock) {
306-
if (hasEnded) {
322+
if (!isModifiableByCurrentThread()) {
307323
logger.log(Level.FINE, "Calling setAttribute() on an ended Span.");
308324
return this;
309325
}
@@ -318,6 +334,12 @@ public <T> ReadWriteSpan setAttribute(AttributeKey<T> key, T value) {
318334
return this;
319335
}
320336

337+
@GuardedBy("lock")
338+
private boolean isModifiableByCurrentThread() {
339+
return hasEnded == EndState.NOT_ENDED
340+
|| (hasEnded == EndState.ENDING && Thread.currentThread() == spanEndingThread);
341+
}
342+
321343
@Override
322344
public ReadWriteSpan addEvent(String name) {
323345
if (name == null) {
@@ -380,7 +402,7 @@ public ReadWriteSpan addEvent(String name, Attributes attributes, long timestamp
380402

381403
private void addTimedEvent(EventData timedEvent) {
382404
synchronized (lock) {
383-
if (hasEnded) {
405+
if (!isModifiableByCurrentThread()) {
384406
logger.log(Level.FINE, "Calling addEvent() on an ended Span.");
385407
return;
386408
}
@@ -400,7 +422,7 @@ public ReadWriteSpan setStatus(StatusCode statusCode, @Nullable String descripti
400422
return this;
401423
}
402424
synchronized (lock) {
403-
if (hasEnded) {
425+
if (!isModifiableByCurrentThread()) {
404426
logger.log(Level.FINE, "Calling setStatus() on an ended Span.");
405427
return this;
406428
} else if (this.status.getStatusCode() == StatusCode.OK) {
@@ -438,7 +460,7 @@ public ReadWriteSpan updateName(String name) {
438460
return this;
439461
}
440462
synchronized (lock) {
441-
if (hasEnded) {
463+
if (!isModifiableByCurrentThread()) {
442464
logger.log(Level.FINE, "Calling updateName() on an ended Span.");
443465
return this;
444466
}
@@ -463,7 +485,7 @@ public Span addLink(SpanContext spanContext, Attributes attributes) {
463485
spanLimits.getMaxNumberOfAttributesPerLink(),
464486
spanLimits.getMaxAttributeValueLength()));
465487
synchronized (lock) {
466-
if (hasEnded) {
488+
if (!isModifiableByCurrentThread()) {
467489
logger.log(Level.FINE, "Calling addLink() on an ended Span.");
468490
return this;
469491
}
@@ -493,12 +515,22 @@ public void end(long timestamp, TimeUnit unit) {
493515

494516
private void endInternal(long endEpochNanos) {
495517
synchronized (lock) {
496-
if (hasEnded) {
497-
logger.log(Level.FINE, "Calling end() on an ended Span.");
518+
if (hasEnded != EndState.NOT_ENDED) {
519+
logger.log(Level.FINE, "Calling end() on an ended or ending Span.");
498520
return;
499521
}
500522
this.endEpochNanos = endEpochNanos;
501-
hasEnded = true;
523+
spanEndingThread = Thread.currentThread();
524+
hasEnded = EndState.ENDING;
525+
}
526+
if (spanProcessor instanceof ExtendedSpanProcessor) {
527+
ExtendedSpanProcessor extendedSpanProcessor = (ExtendedSpanProcessor) spanProcessor;
528+
if (extendedSpanProcessor.isOnEndingRequired()) {
529+
extendedSpanProcessor.onEnding(this);
530+
}
531+
}
532+
synchronized (lock) {
533+
hasEnded = EndState.ENDED;
502534
}
503535
if (spanProcessor.isEndRequired()) {
504536
spanProcessor.onEnd(this);
@@ -508,7 +540,7 @@ private void endInternal(long endEpochNanos) {
508540
@Override
509541
public boolean isRecording() {
510542
synchronized (lock) {
511-
return !hasEnded;
543+
return hasEnded != EndState.ENDED;
512544
}
513545
}
514546

@@ -533,7 +565,7 @@ private List<EventData> getImmutableTimedEvents() {
533565

534566
// if the span has ended, then the events are unmodifiable
535567
// so we can return them directly and save copying all the data.
536-
if (hasEnded) {
568+
if (hasEnded == EndState.ENDED) {
537569
return Collections.unmodifiableList(events);
538570
}
539571

@@ -547,7 +579,7 @@ private Attributes getImmutableAttributes() {
547579
}
548580
// if the span has ended, then the attributes are unmodifiable,
549581
// so we can return them directly and save copying all the data.
550-
if (hasEnded) {
582+
if (hasEnded == EndState.ENDED) {
551583
return attributes;
552584
}
553585
// otherwise, make a copy of the data into an immutable container.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.trace.internal;
7+
8+
import io.opentelemetry.api.trace.Span;
9+
import io.opentelemetry.sdk.trace.ReadWriteSpan;
10+
import io.opentelemetry.sdk.trace.ReadableSpan;
11+
import io.opentelemetry.sdk.trace.SpanProcessor;
12+
13+
/**
14+
* Extended {@link SpanProcessor} with experimental APIs.
15+
*
16+
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
17+
* at any time.
18+
*/
19+
public interface ExtendedSpanProcessor extends SpanProcessor {
20+
21+
/**
22+
* Called when a {@link io.opentelemetry.api.trace.Span} is ended, but before {@link
23+
* SpanProcessor#onEnd(ReadableSpan)} is invoked with an immutable variant of this span. This
24+
* means that the span will still be mutable. Note that the span will only be modifiable
25+
* synchronously from this callback, concurrent modifications from other threads will be
26+
* prevented. Only called if {@link Span#isRecording()} returns true.
27+
*
28+
* <p>This method is called synchronously on the execution thread, should not throw or block the
29+
* execution thread.
30+
*
31+
* @param span the {@code Span} that is just about to be ended.
32+
*/
33+
void onEnding(ReadWriteSpan span);
34+
35+
/**
36+
* Returns {@code true} if this {@link SpanProcessor} requires onEnding events.
37+
*
38+
* @return {@code true} if this {@link SpanProcessor} requires onEnding events.
39+
*/
40+
boolean isOnEndingRequired();
41+
}

sdk/trace/src/test/java/io/opentelemetry/sdk/trace/MultiSpanProcessorTest.java

+21-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import io.opentelemetry.context.Context;
1616
import io.opentelemetry.sdk.common.CompletableResultCode;
17+
import io.opentelemetry.sdk.trace.internal.ExtendedSpanProcessor;
1718
import java.util.Arrays;
1819
import java.util.Collections;
1920
import org.junit.jupiter.api.BeforeEach;
@@ -27,18 +28,20 @@
2728
@ExtendWith(MockitoExtension.class)
2829
@MockitoSettings(strictness = Strictness.LENIENT)
2930
class MultiSpanProcessorTest {
30-
@Mock private SpanProcessor spanProcessor1;
31-
@Mock private SpanProcessor spanProcessor2;
31+
@Mock private ExtendedSpanProcessor spanProcessor1;
32+
@Mock private ExtendedSpanProcessor spanProcessor2;
3233
@Mock private ReadableSpan readableSpan;
3334
@Mock private ReadWriteSpan readWriteSpan;
3435

3536
@BeforeEach
3637
void setUp() {
3738
when(spanProcessor1.isStartRequired()).thenReturn(true);
39+
when(spanProcessor1.isOnEndingRequired()).thenReturn(true);
3840
when(spanProcessor1.isEndRequired()).thenReturn(true);
3941
when(spanProcessor1.forceFlush()).thenReturn(CompletableResultCode.ofSuccess());
4042
when(spanProcessor1.shutdown()).thenReturn(CompletableResultCode.ofSuccess());
4143
when(spanProcessor2.isStartRequired()).thenReturn(true);
44+
when(spanProcessor2.isOnEndingRequired()).thenReturn(true);
4245
when(spanProcessor2.isEndRequired()).thenReturn(true);
4346
when(spanProcessor2.forceFlush()).thenReturn(CompletableResultCode.ofSuccess());
4447
when(spanProcessor2.shutdown()).thenReturn(CompletableResultCode.ofSuccess());
@@ -61,12 +64,17 @@ void oneSpanProcessor() {
6164

6265
@Test
6366
void twoSpanProcessor() {
64-
SpanProcessor multiSpanProcessor =
65-
SpanProcessor.composite(Arrays.asList(spanProcessor1, spanProcessor2));
67+
ExtendedSpanProcessor multiSpanProcessor =
68+
(ExtendedSpanProcessor)
69+
SpanProcessor.composite(Arrays.asList(spanProcessor1, spanProcessor2));
6670
multiSpanProcessor.onStart(Context.root(), readWriteSpan);
6771
verify(spanProcessor1).onStart(same(Context.root()), same(readWriteSpan));
6872
verify(spanProcessor2).onStart(same(Context.root()), same(readWriteSpan));
6973

74+
multiSpanProcessor.onEnding(readWriteSpan);
75+
verify(spanProcessor1).onEnding(same(readWriteSpan));
76+
verify(spanProcessor2).onEnding(same(readWriteSpan));
77+
7078
multiSpanProcessor.onEnd(readableSpan);
7179
verify(spanProcessor1).onEnd(same(readableSpan));
7280
verify(spanProcessor2).onEnd(same(readableSpan));
@@ -83,9 +91,11 @@ void twoSpanProcessor() {
8391
@Test
8492
void twoSpanProcessor_DifferentRequirements() {
8593
when(spanProcessor1.isEndRequired()).thenReturn(false);
94+
when(spanProcessor2.isOnEndingRequired()).thenReturn(false);
8695
when(spanProcessor2.isStartRequired()).thenReturn(false);
87-
SpanProcessor multiSpanProcessor =
88-
SpanProcessor.composite(Arrays.asList(spanProcessor1, spanProcessor2));
96+
ExtendedSpanProcessor multiSpanProcessor =
97+
(ExtendedSpanProcessor)
98+
SpanProcessor.composite(Arrays.asList(spanProcessor1, spanProcessor2));
8999

90100
assertThat(multiSpanProcessor.isStartRequired()).isTrue();
91101
assertThat(multiSpanProcessor.isEndRequired()).isTrue();
@@ -94,6 +104,10 @@ void twoSpanProcessor_DifferentRequirements() {
94104
verify(spanProcessor1).onStart(same(Context.root()), same(readWriteSpan));
95105
verify(spanProcessor2, times(0)).onStart(any(Context.class), any(ReadWriteSpan.class));
96106

107+
multiSpanProcessor.onEnding(readWriteSpan);
108+
verify(spanProcessor1).onEnding(same(readWriteSpan));
109+
verify(spanProcessor2, times(0)).onEnding(any(ReadWriteSpan.class));
110+
97111
multiSpanProcessor.onEnd(readableSpan);
98112
verify(spanProcessor1, times(0)).onEnd(any(ReadableSpan.class));
99113
verify(spanProcessor2).onEnd(same(readableSpan));
@@ -117,6 +131,7 @@ void stringRepresentation() {
117131
.hasToString(
118132
"MultiSpanProcessor{"
119133
+ "spanProcessorsStart=[spanProcessor1, spanProcessor1], "
134+
+ "spanProcessorsEnding=[spanProcessor1, spanProcessor1], "
120135
+ "spanProcessorsEnd=[spanProcessor1, spanProcessor1], "
121136
+ "spanProcessorsAll=[spanProcessor1, spanProcessor1]}");
122137
}

0 commit comments

Comments
 (0)