Skip to content

Commit 6ca397a

Browse files
authored
Allow customization of parent-override behaviour for inferred-spans (#1533)
1 parent 4c72e59 commit 6ca397a

File tree

9 files changed

+112
-17
lines changed

9 files changed

+112
-17
lines changed

inferred-spans/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ So if you are using an autoconfigured OpenTelemetry SDK, you'll only need to add
4141
| otel.inferred.spans.interval <br/> OTEL_INFERRED_SPANS_INTERVAL | `5s` | The interval at which profiling sessions should be started. |
4242
| otel.inferred.spans.duration <br/> OTEL_INFERRED_SPANS_DURATION | `5s` | The duration of a profiling session. For sampled transactions which fall within a profiling session (they start after and end before the session), so-called inferred spans will be created. They appear in the trace waterfall view like regular spans. <br/> NOTE: It is not recommended to set much higher durations as it may fill the activation events file and async-profiler's frame buffer. Warnings will be logged if the activation events file is full. If you want to have more profiling coverage, try decreasing `profiling_inferred_spans_interval` |
4343
| otel.inferred.spans.lib.directory <br/> OTEL_INFERRED_SPANS_LIB_DIRECTORY | Defaults to the value of `java.io.tmpdir` | Profiling requires that the [async-profiler](https://github.com/async-profiler/async-profiler) shared library is exported to a temporary location and loaded by the JVM. The partition backing this location must be executable, however in some server-hardened environments, `noexec` may be set on the standard `/tmp` partition, leading to `java.lang.UnsatisfiedLinkError` errors. Set this property to an alternative directory (e.g. `/var/tmp`) to resolve this. |
44+
| otel.inferred.spans.duration <br/> OTEL_INFERRED_SPANS_DURATION | `5s` | The duration of a profiling session. For sampled transactions which fall within a profiling session (they start after and end before the session), so-called inferred spans will be created. They appear in the trace waterfall view like regular spans. <br/> NOTE: It is not recommended to set much higher durations as it may fill the activation events file and async-profiler's frame buffer. Warnings will be logged if the activation events file is full. If you want to have more profiling coverage, try decreasing `profiling_inferred_spans_interval` |
45+
| otel.inferred.spans.parent.override.handler <br/> OTEL_INFERRED_SPANS_PARENT_OVERRIDE_HANDLER | Defaults to a handler adding span-links to the inferred span | Inferred spans sometimes need to be inserted as the new parent of a normal span, which is not directly possible because that span has already been sent. For this reason, this relationship needs to be represented differently, which normally is done by adding a span-link to the inferred span. This configuration can be used to override that behaviour by providing the fully qualified name of a class implementing `BiConsumer<SpanBuilder, SpanContext>`: The biconsumer will be invoked with the inferred span as first argument and the span for which the inferred one was detected as new parent as second argument |
4446

4547
### Manual SDK setup
4648

inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfig.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
package io.opentelemetry.contrib.inferredspans;
77

88
import com.google.auto.service.AutoService;
9+
import io.opentelemetry.api.trace.SpanBuilder;
10+
import io.opentelemetry.api.trace.SpanContext;
911
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer;
1012
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider;
1113
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
1214
import java.time.Duration;
1315
import java.util.Arrays;
1416
import java.util.List;
17+
import java.util.function.BiConsumer;
1518
import java.util.function.Consumer;
1619
import java.util.logging.Logger;
1720
import java.util.stream.Collectors;
@@ -34,6 +37,8 @@ public class InferredSpansAutoConfig implements AutoConfigurationCustomizerProvi
3437
static final String INTERVAL_OPTION = "otel.inferred.spans.interval";
3538
static final String DURATION_OPTION = "otel.inferred.spans.duration";
3639
static final String LIB_DIRECTORY_OPTION = "otel.inferred.spans.lib.directory";
40+
static final String PARENT_OVERRIDE_HANDLER_OPTION =
41+
"otel.inferred.spans.parent.override.handler";
3742

3843
@Override
3944
public void customize(AutoConfigurationCustomizer config) {
@@ -56,6 +61,12 @@ public void customize(AutoConfigurationCustomizer config) {
5661
applier.applyDuration(DURATION_OPTION, builder::profilingDuration);
5762
applier.applyString(LIB_DIRECTORY_OPTION, builder::profilerLibDirectory);
5863

64+
String parentOverrideHandlerName = properties.getString(PARENT_OVERRIDE_HANDLER_OPTION);
65+
if (parentOverrideHandlerName != null && !parentOverrideHandlerName.isEmpty()) {
66+
builder.parentOverrideHandler(
67+
constructParentOverrideHandler(parentOverrideHandlerName));
68+
}
69+
5970
providerBuilder.addSpanProcessor(builder.build());
6071
} else {
6172
log.finest(
@@ -65,6 +76,16 @@ public void customize(AutoConfigurationCustomizer config) {
6576
});
6677
}
6778

79+
@SuppressWarnings("unchecked")
80+
private static BiConsumer<SpanBuilder, SpanContext> constructParentOverrideHandler(String name) {
81+
try {
82+
Class<?> clazz = Class.forName(name);
83+
return (BiConsumer<SpanBuilder, SpanContext>) clazz.getConstructor().newInstance();
84+
} catch (Exception e) {
85+
throw new IllegalArgumentException("Could not construct parent override handler", e);
86+
}
87+
}
88+
6889
private static class PropertiesApplier {
6990

7091
private final ConfigProperties properties;

inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansProcessorBuilder.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55

66
package io.opentelemetry.contrib.inferredspans;
77

8+
import io.opentelemetry.api.trace.SpanBuilder;
9+
import io.opentelemetry.api.trace.SpanContext;
10+
import io.opentelemetry.contrib.inferredspans.internal.CallTree;
811
import io.opentelemetry.contrib.inferredspans.internal.InferredSpansConfiguration;
912
import io.opentelemetry.contrib.inferredspans.internal.SpanAnchoredClock;
1013
import java.io.File;
1114
import java.time.Duration;
1215
import java.util.Arrays;
1316
import java.util.List;
17+
import java.util.function.BiConsumer;
1418
import javax.annotation.Nullable;
1519

1620
@SuppressWarnings("CanIgnoreReturnValueSuggester")
@@ -48,6 +52,8 @@ public class InferredSpansProcessorBuilder {
4852

4953
@Nullable private File activationEventsFile = null;
5054
@Nullable private File jfrFile = null;
55+
private BiConsumer<SpanBuilder, SpanContext> parentOverrideHandler =
56+
CallTree.DEFAULT_PARENT_OVERRIDE;
5157

5258
InferredSpansProcessorBuilder() {}
5359

@@ -64,7 +70,8 @@ public InferredSpansProcessor build() {
6470
excludedClasses,
6571
profilerInterval,
6672
profilingDuration,
67-
profilerLibDirectory);
73+
profilerLibDirectory,
74+
parentOverrideHandler);
6875
return new InferredSpansProcessor(
6976
config, clock, startScheduledProfiling, activationEventsFile, jfrFile);
7077
}
@@ -188,4 +195,17 @@ InferredSpansProcessorBuilder jfrFile(@Nullable File jfrFile) {
188195
this.jfrFile = jfrFile;
189196
return this;
190197
}
198+
199+
/**
200+
* Defines the action to perform when a inferred span is discovered to actually be the parent of a
201+
* normal span. The first argument of the handler is the modifiable inferred span, the second
202+
* argument the span context of the normal span which should be somehow marked as child of the
203+
* inferred one. By default, a span link is added to the inferred span to represent this
204+
* relationship.
205+
*/
206+
InferredSpansProcessorBuilder parentOverrideHandler(
207+
BiConsumer<SpanBuilder, SpanContext> handler) {
208+
this.parentOverrideHandler = handler;
209+
return this;
210+
}
191211
}

inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/CallTree.java

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Arrays;
2626
import java.util.List;
2727
import java.util.concurrent.TimeUnit;
28+
import java.util.function.BiConsumer;
2829
import java.util.logging.Logger;
2930
import javax.annotation.Nullable;
3031
import org.agrona.collections.LongHashSet;
@@ -50,9 +51,12 @@ public class CallTree implements Recyclable {
5051

5152
private static final int INITIAL_CHILD_SIZE = 2;
5253

53-
private static final Attributes CHILD_LINK_ATTRIBUTES =
54+
public static final Attributes CHILD_LINK_ATTRIBUTES =
5455
Attributes.builder().put(LINK_IS_CHILD, true).build();
5556

57+
public static final BiConsumer<SpanBuilder, SpanContext> DEFAULT_PARENT_OVERRIDE =
58+
(inferredSpan, child) -> inferredSpan.addLink(child, CHILD_LINK_ATTRIBUTES);
59+
5660
@Nullable private CallTree parent;
5761
protected int count;
5862
private List<CallTree> children = new ArrayList<>(INITIAL_CHILD_SIZE);
@@ -427,6 +431,7 @@ int spanify(
427431
@Nullable Span parentSpan,
428432
TraceContext parentContext,
429433
SpanAnchoredClock clock,
434+
BiConsumer<SpanBuilder, SpanContext> spanParentOverride,
430435
StringBuilder tempBuilder,
431436
Tracer tracer) {
432437
int createdSpans = 0;
@@ -437,7 +442,8 @@ int spanify(
437442
Span span = null;
438443
if (!isPillar() || isLeaf()) {
439444
createdSpans++;
440-
span = asSpan(root, parentSpan, parentContext, tracer, clock, tempBuilder);
445+
span =
446+
asSpan(root, parentSpan, parentContext, tracer, clock, spanParentOverride, tempBuilder);
441447
this.isSpan = true;
442448
}
443449
List<CallTree> children = getChildren();
@@ -450,6 +456,7 @@ int spanify(
450456
span != null ? span : parentSpan,
451457
parentContext,
452458
clock,
459+
spanParentOverride,
453460
tempBuilder,
454461
tracer);
455462
}
@@ -462,6 +469,7 @@ protected Span asSpan(
462469
TraceContext parentContext,
463470
Tracer tracer,
464471
SpanAnchoredClock clock,
472+
BiConsumer<SpanBuilder, SpanContext> spanParentOverride,
465473
StringBuilder tempBuilder) {
466474

467475
Context parentOtelCtx;
@@ -494,7 +502,11 @@ protected Span asSpan(
494502
clock.toEpochNanos(parentContext.getClockAnchor(), this.start),
495503
TimeUnit.NANOSECONDS);
496504
insertChildIdLinks(
497-
spanBuilder, Span.fromContext(parentOtelCtx).getSpanContext(), parentContext, tempBuilder);
505+
spanBuilder,
506+
Span.fromContext(parentOtelCtx).getSpanContext(),
507+
parentContext,
508+
spanParentOverride,
509+
tempBuilder);
498510

499511
// we're not interested in the very bottom of the stack which contains things like accepting and
500512
// handling connections
@@ -517,6 +529,7 @@ private void insertChildIdLinks(
517529
SpanBuilder span,
518530
SpanContext parentContext,
519531
TraceContext nonInferredParent,
532+
BiConsumer<SpanBuilder, SpanContext> spanParentOverride,
520533
StringBuilder tempBuilder) {
521534
if (childIds == null || childIds.isEmpty()) {
522535
return;
@@ -527,13 +540,13 @@ private void insertChildIdLinks(
527540
if (nonInferredParent.getSpanId() == childIds.getParentId(i)) {
528541
tempBuilder.setLength(0);
529542
HexUtils.appendLongAsHex(childIds.getId(i), tempBuilder);
530-
SpanContext spanContext =
543+
SpanContext childSpanContext =
531544
SpanContext.create(
532545
parentContext.getTraceId(),
533546
tempBuilder.toString(),
534547
parentContext.getTraceFlags(),
535548
parentContext.getTraceState());
536-
span.addLink(spanContext, CHILD_LINK_ATTRIBUTES);
549+
spanParentOverride.accept(span, childSpanContext);
537550
}
538551
}
539552
}
@@ -863,13 +876,18 @@ private static CallTree findCommonAncestor(CallTree previousTopOfStack, CallTree
863876
* possible to update the parent ID of a regular span so that it correctly reflects being a
864877
* child of an inferred span.
865878
*/
866-
public int spanify(SpanAnchoredClock clock, Tracer tracer) {
879+
public int spanify(
880+
SpanAnchoredClock clock,
881+
Tracer tracer,
882+
BiConsumer<SpanBuilder, SpanContext> normalSpanOverride) {
867883
StringBuilder tempBuilder = new StringBuilder();
868884
int createdSpans = 0;
869885
List<CallTree> callTrees = getChildren();
870886
for (int i = 0, size = callTrees.size(); i < size; i++) {
871887
createdSpans +=
872-
callTrees.get(i).spanify(this, null, rootContext, clock, tempBuilder, tracer);
888+
callTrees
889+
.get(i)
890+
.spanify(this, null, rootContext, clock, normalSpanOverride, tempBuilder, tracer);
873891
}
874892
return createdSpans;
875893
}

inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/InferredSpansConfiguration.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55

66
package io.opentelemetry.contrib.inferredspans.internal;
77

8+
import io.opentelemetry.api.trace.SpanBuilder;
9+
import io.opentelemetry.api.trace.SpanContext;
810
import io.opentelemetry.contrib.inferredspans.WildcardMatcher;
911
import java.time.Duration;
1012
import java.util.List;
13+
import java.util.function.BiConsumer;
1114
import javax.annotation.Nullable;
1215

1316
public class InferredSpansConfiguration {
@@ -22,8 +25,8 @@ public class InferredSpansConfiguration {
2225
private final List<WildcardMatcher> excludedClasses;
2326
private final Duration profilerInterval;
2427
private final Duration profilingDuration;
25-
2628
@Nullable private final String profilerLibDirectory;
29+
private final BiConsumer<SpanBuilder, SpanContext> parentOverrideHandler;
2730

2831
@SuppressWarnings("TooManyParameters")
2932
public InferredSpansConfiguration(
@@ -37,7 +40,8 @@ public InferredSpansConfiguration(
3740
List<WildcardMatcher> excludedClasses,
3841
Duration profilerInterval,
3942
Duration profilingDuration,
40-
@Nullable String profilerLibDirectory) {
43+
@Nullable String profilerLibDirectory,
44+
BiConsumer<SpanBuilder, SpanContext> parentOverrideHandler) {
4145
this.profilerLoggingEnabled = profilerLoggingEnabled;
4246
this.backupDiagnosticFiles = backupDiagnosticFiles;
4347
this.asyncProfilerSafeMode = asyncProfilerSafeMode;
@@ -49,6 +53,7 @@ public InferredSpansConfiguration(
4953
this.profilerInterval = profilerInterval;
5054
this.profilingDuration = profilingDuration;
5155
this.profilerLibDirectory = profilerLibDirectory;
56+
this.parentOverrideHandler = parentOverrideHandler;
5257
}
5358

5459
public boolean isProfilingLoggingEnabled() {
@@ -100,4 +105,8 @@ public String getProfilerLibDirectory() {
100105
public boolean isPostProcessingEnabled() {
101106
return postProcessingEnabled;
102107
}
108+
109+
public BiConsumer<SpanBuilder, SpanContext> getParentOverrideHandler() {
110+
return parentOverrideHandler;
111+
}
103112
}

inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/SamplingProfiler.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -952,7 +952,10 @@ private void stopProfiling(SamplingProfiler samplingProfiler) {
952952
callTree.end(
953953
samplingProfiler.callTreePool, samplingProfiler.getInferredSpansMinDurationNs());
954954
int createdSpans =
955-
callTree.spanify(samplingProfiler.getClock(), samplingProfiler.tracerProvider.get());
955+
callTree.spanify(
956+
samplingProfiler.getClock(),
957+
samplingProfiler.tracerProvider.get(),
958+
samplingProfiler.config.getParentOverrideHandler());
956959
if (logger.isLoggable(Level.FINE)) {
957960
if (createdSpans > 0) {
958961
logger.log(

inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfigTest.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import io.opentelemetry.api.GlobalOpenTelemetry;
1212
import io.opentelemetry.api.OpenTelemetry;
1313
import io.opentelemetry.api.trace.Span;
14+
import io.opentelemetry.api.trace.SpanBuilder;
15+
import io.opentelemetry.api.trace.SpanContext;
1416
import io.opentelemetry.api.trace.Tracer;
1517
import io.opentelemetry.context.Scope;
1618
import io.opentelemetry.contrib.inferredspans.internal.InferredSpansConfiguration;
@@ -23,6 +25,7 @@
2325
import java.nio.file.Path;
2426
import java.time.Duration;
2527
import java.util.List;
28+
import java.util.function.BiConsumer;
2629
import java.util.stream.Collectors;
2730
import org.junit.jupiter.api.AfterEach;
2831
import org.junit.jupiter.api.BeforeEach;
@@ -40,6 +43,12 @@ public void resetGlobalOtel() {
4043
OtelReflectionUtils.shutdownAndResetGlobalOtel();
4144
}
4245

46+
public static class NoOpParentOverrideHandler implements BiConsumer<SpanBuilder, SpanContext> {
47+
48+
@Override
49+
public void accept(SpanBuilder spanBuilder, SpanContext spanContext) {}
50+
}
51+
4352
@Test
4453
@DisabledOnOpenJ9
4554
public void checkAllOptions(@TempDir Path tmpDir) {
@@ -57,7 +66,10 @@ public void checkAllOptions(@TempDir Path tmpDir) {
5766
.put(InferredSpansAutoConfig.EXCLUDED_CLASSES_OPTION, "blub,test*.test2")
5867
.put(InferredSpansAutoConfig.INTERVAL_OPTION, "2s")
5968
.put(InferredSpansAutoConfig.DURATION_OPTION, "3s")
60-
.put(InferredSpansAutoConfig.LIB_DIRECTORY_OPTION, libDir)) {
69+
.put(InferredSpansAutoConfig.LIB_DIRECTORY_OPTION, libDir)
70+
.put(
71+
InferredSpansAutoConfig.PARENT_OVERRIDE_HANDLER_OPTION,
72+
NoOpParentOverrideHandler.class.getName())) {
6173

6274
OpenTelemetry otel = GlobalOpenTelemetry.get();
6375
List<SpanProcessor> processors = OtelReflectionUtils.getSpanProcessors(otel);
@@ -81,6 +93,7 @@ public void checkAllOptions(@TempDir Path tmpDir) {
8193
assertThat(config.getProfilingInterval()).isEqualTo(Duration.ofSeconds(2));
8294
assertThat(config.getProfilingDuration()).isEqualTo(Duration.ofSeconds(3));
8395
assertThat(config.getProfilerLibDirectory()).isEqualTo(libDir);
96+
assertThat(config.getParentOverrideHandler()).isInstanceOf(NoOpParentOverrideHandler.class);
8497
}
8598
}
8699

0 commit comments

Comments
 (0)