From 6ca397a839077ea05c55742c127b7aa38da1837f Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Thu, 21 Nov 2024 00:45:20 +0100 Subject: [PATCH] Allow customization of parent-override behaviour for inferred-spans (#1533) --- inferred-spans/README.md | 2 ++ .../InferredSpansAutoConfig.java | 21 ++++++++++++ .../InferredSpansProcessorBuilder.java | 22 ++++++++++++- .../inferredspans/internal/CallTree.java | 32 +++++++++++++++---- .../internal/InferredSpansConfiguration.java | 13 ++++++-- .../internal/SamplingProfiler.java | 5 ++- .../InferredSpansAutoConfigTest.java | 15 ++++++++- .../internal/CallTreeSpanifyTest.java | 14 +++++--- .../inferredspans/internal/CallTreeTest.java | 5 ++- 9 files changed, 112 insertions(+), 17 deletions(-) diff --git a/inferred-spans/README.md b/inferred-spans/README.md index 33560a9aa..56ae21a5b 100644 --- a/inferred-spans/README.md +++ b/inferred-spans/README.md @@ -41,6 +41,8 @@ So if you are using an autoconfigured OpenTelemetry SDK, you'll only need to add | otel.inferred.spans.interval
OTEL_INFERRED_SPANS_INTERVAL | `5s` | The interval at which profiling sessions should be started. | | otel.inferred.spans.duration
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.
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` | | otel.inferred.spans.lib.directory
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. | +| otel.inferred.spans.duration
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.
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` | +| otel.inferred.spans.parent.override.handler
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`: 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 | ### Manual SDK setup diff --git a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfig.java b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfig.java index a64bb2641..6a4e1ba00 100644 --- a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfig.java +++ b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfig.java @@ -6,12 +6,15 @@ package io.opentelemetry.contrib.inferredspans; import com.google.auto.service.AutoService; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.time.Duration; import java.util.Arrays; import java.util.List; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -34,6 +37,8 @@ public class InferredSpansAutoConfig implements AutoConfigurationCustomizerProvi static final String INTERVAL_OPTION = "otel.inferred.spans.interval"; static final String DURATION_OPTION = "otel.inferred.spans.duration"; static final String LIB_DIRECTORY_OPTION = "otel.inferred.spans.lib.directory"; + static final String PARENT_OVERRIDE_HANDLER_OPTION = + "otel.inferred.spans.parent.override.handler"; @Override public void customize(AutoConfigurationCustomizer config) { @@ -56,6 +61,12 @@ public void customize(AutoConfigurationCustomizer config) { applier.applyDuration(DURATION_OPTION, builder::profilingDuration); applier.applyString(LIB_DIRECTORY_OPTION, builder::profilerLibDirectory); + String parentOverrideHandlerName = properties.getString(PARENT_OVERRIDE_HANDLER_OPTION); + if (parentOverrideHandlerName != null && !parentOverrideHandlerName.isEmpty()) { + builder.parentOverrideHandler( + constructParentOverrideHandler(parentOverrideHandlerName)); + } + providerBuilder.addSpanProcessor(builder.build()); } else { log.finest( @@ -65,6 +76,16 @@ public void customize(AutoConfigurationCustomizer config) { }); } + @SuppressWarnings("unchecked") + private static BiConsumer constructParentOverrideHandler(String name) { + try { + Class clazz = Class.forName(name); + return (BiConsumer) clazz.getConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException("Could not construct parent override handler", e); + } + } + private static class PropertiesApplier { private final ConfigProperties properties; diff --git a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansProcessorBuilder.java b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansProcessorBuilder.java index de28a8709..3e487bd6b 100644 --- a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansProcessorBuilder.java +++ b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansProcessorBuilder.java @@ -5,12 +5,16 @@ package io.opentelemetry.contrib.inferredspans; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.contrib.inferredspans.internal.CallTree; import io.opentelemetry.contrib.inferredspans.internal.InferredSpansConfiguration; import io.opentelemetry.contrib.inferredspans.internal.SpanAnchoredClock; import java.io.File; import java.time.Duration; import java.util.Arrays; import java.util.List; +import java.util.function.BiConsumer; import javax.annotation.Nullable; @SuppressWarnings("CanIgnoreReturnValueSuggester") @@ -48,6 +52,8 @@ public class InferredSpansProcessorBuilder { @Nullable private File activationEventsFile = null; @Nullable private File jfrFile = null; + private BiConsumer parentOverrideHandler = + CallTree.DEFAULT_PARENT_OVERRIDE; InferredSpansProcessorBuilder() {} @@ -64,7 +70,8 @@ public InferredSpansProcessor build() { excludedClasses, profilerInterval, profilingDuration, - profilerLibDirectory); + profilerLibDirectory, + parentOverrideHandler); return new InferredSpansProcessor( config, clock, startScheduledProfiling, activationEventsFile, jfrFile); } @@ -188,4 +195,17 @@ InferredSpansProcessorBuilder jfrFile(@Nullable File jfrFile) { this.jfrFile = jfrFile; return this; } + + /** + * Defines the action to perform when a inferred span is discovered to actually be the parent of a + * normal span. The first argument of the handler is the modifiable inferred span, the second + * argument the span context of the normal span which should be somehow marked as child of the + * inferred one. By default, a span link is added to the inferred span to represent this + * relationship. + */ + InferredSpansProcessorBuilder parentOverrideHandler( + BiConsumer handler) { + this.parentOverrideHandler = handler; + return this; + } } diff --git a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/CallTree.java b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/CallTree.java index fa8540e30..74468a3fd 100644 --- a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/CallTree.java +++ b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/CallTree.java @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; import java.util.logging.Logger; import javax.annotation.Nullable; import org.agrona.collections.LongHashSet; @@ -50,9 +51,12 @@ public class CallTree implements Recyclable { private static final int INITIAL_CHILD_SIZE = 2; - private static final Attributes CHILD_LINK_ATTRIBUTES = + public static final Attributes CHILD_LINK_ATTRIBUTES = Attributes.builder().put(LINK_IS_CHILD, true).build(); + public static final BiConsumer DEFAULT_PARENT_OVERRIDE = + (inferredSpan, child) -> inferredSpan.addLink(child, CHILD_LINK_ATTRIBUTES); + @Nullable private CallTree parent; protected int count; private List children = new ArrayList<>(INITIAL_CHILD_SIZE); @@ -427,6 +431,7 @@ int spanify( @Nullable Span parentSpan, TraceContext parentContext, SpanAnchoredClock clock, + BiConsumer spanParentOverride, StringBuilder tempBuilder, Tracer tracer) { int createdSpans = 0; @@ -437,7 +442,8 @@ int spanify( Span span = null; if (!isPillar() || isLeaf()) { createdSpans++; - span = asSpan(root, parentSpan, parentContext, tracer, clock, tempBuilder); + span = + asSpan(root, parentSpan, parentContext, tracer, clock, spanParentOverride, tempBuilder); this.isSpan = true; } List children = getChildren(); @@ -450,6 +456,7 @@ int spanify( span != null ? span : parentSpan, parentContext, clock, + spanParentOverride, tempBuilder, tracer); } @@ -462,6 +469,7 @@ protected Span asSpan( TraceContext parentContext, Tracer tracer, SpanAnchoredClock clock, + BiConsumer spanParentOverride, StringBuilder tempBuilder) { Context parentOtelCtx; @@ -494,7 +502,11 @@ protected Span asSpan( clock.toEpochNanos(parentContext.getClockAnchor(), this.start), TimeUnit.NANOSECONDS); insertChildIdLinks( - spanBuilder, Span.fromContext(parentOtelCtx).getSpanContext(), parentContext, tempBuilder); + spanBuilder, + Span.fromContext(parentOtelCtx).getSpanContext(), + parentContext, + spanParentOverride, + tempBuilder); // we're not interested in the very bottom of the stack which contains things like accepting and // handling connections @@ -517,6 +529,7 @@ private void insertChildIdLinks( SpanBuilder span, SpanContext parentContext, TraceContext nonInferredParent, + BiConsumer spanParentOverride, StringBuilder tempBuilder) { if (childIds == null || childIds.isEmpty()) { return; @@ -527,13 +540,13 @@ private void insertChildIdLinks( if (nonInferredParent.getSpanId() == childIds.getParentId(i)) { tempBuilder.setLength(0); HexUtils.appendLongAsHex(childIds.getId(i), tempBuilder); - SpanContext spanContext = + SpanContext childSpanContext = SpanContext.create( parentContext.getTraceId(), tempBuilder.toString(), parentContext.getTraceFlags(), parentContext.getTraceState()); - span.addLink(spanContext, CHILD_LINK_ATTRIBUTES); + spanParentOverride.accept(span, childSpanContext); } } } @@ -863,13 +876,18 @@ private static CallTree findCommonAncestor(CallTree previousTopOfStack, CallTree * possible to update the parent ID of a regular span so that it correctly reflects being a * child of an inferred span. */ - public int spanify(SpanAnchoredClock clock, Tracer tracer) { + public int spanify( + SpanAnchoredClock clock, + Tracer tracer, + BiConsumer normalSpanOverride) { StringBuilder tempBuilder = new StringBuilder(); int createdSpans = 0; List callTrees = getChildren(); for (int i = 0, size = callTrees.size(); i < size; i++) { createdSpans += - callTrees.get(i).spanify(this, null, rootContext, clock, tempBuilder, tracer); + callTrees + .get(i) + .spanify(this, null, rootContext, clock, normalSpanOverride, tempBuilder, tracer); } return createdSpans; } diff --git a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/InferredSpansConfiguration.java b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/InferredSpansConfiguration.java index 7652982c3..5091a36a5 100644 --- a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/InferredSpansConfiguration.java +++ b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/InferredSpansConfiguration.java @@ -5,9 +5,12 @@ package io.opentelemetry.contrib.inferredspans.internal; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.contrib.inferredspans.WildcardMatcher; import java.time.Duration; import java.util.List; +import java.util.function.BiConsumer; import javax.annotation.Nullable; public class InferredSpansConfiguration { @@ -22,8 +25,8 @@ public class InferredSpansConfiguration { private final List excludedClasses; private final Duration profilerInterval; private final Duration profilingDuration; - @Nullable private final String profilerLibDirectory; + private final BiConsumer parentOverrideHandler; @SuppressWarnings("TooManyParameters") public InferredSpansConfiguration( @@ -37,7 +40,8 @@ public InferredSpansConfiguration( List excludedClasses, Duration profilerInterval, Duration profilingDuration, - @Nullable String profilerLibDirectory) { + @Nullable String profilerLibDirectory, + BiConsumer parentOverrideHandler) { this.profilerLoggingEnabled = profilerLoggingEnabled; this.backupDiagnosticFiles = backupDiagnosticFiles; this.asyncProfilerSafeMode = asyncProfilerSafeMode; @@ -49,6 +53,7 @@ public InferredSpansConfiguration( this.profilerInterval = profilerInterval; this.profilingDuration = profilingDuration; this.profilerLibDirectory = profilerLibDirectory; + this.parentOverrideHandler = parentOverrideHandler; } public boolean isProfilingLoggingEnabled() { @@ -100,4 +105,8 @@ public String getProfilerLibDirectory() { public boolean isPostProcessingEnabled() { return postProcessingEnabled; } + + public BiConsumer getParentOverrideHandler() { + return parentOverrideHandler; + } } diff --git a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/SamplingProfiler.java b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/SamplingProfiler.java index b3282ae0b..d05496084 100644 --- a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/SamplingProfiler.java +++ b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/internal/SamplingProfiler.java @@ -952,7 +952,10 @@ private void stopProfiling(SamplingProfiler samplingProfiler) { callTree.end( samplingProfiler.callTreePool, samplingProfiler.getInferredSpansMinDurationNs()); int createdSpans = - callTree.spanify(samplingProfiler.getClock(), samplingProfiler.tracerProvider.get()); + callTree.spanify( + samplingProfiler.getClock(), + samplingProfiler.tracerProvider.get(), + samplingProfiler.config.getParentOverrideHandler()); if (logger.isLoggable(Level.FINE)) { if (createdSpans > 0) { logger.log( diff --git a/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfigTest.java b/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfigTest.java index 432d9f84f..1b6a7e3e2 100644 --- a/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfigTest.java +++ b/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/InferredSpansAutoConfigTest.java @@ -11,6 +11,8 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Scope; import io.opentelemetry.contrib.inferredspans.internal.InferredSpansConfiguration; @@ -23,6 +25,7 @@ import java.nio.file.Path; import java.time.Duration; import java.util.List; +import java.util.function.BiConsumer; import java.util.stream.Collectors; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -40,6 +43,12 @@ public void resetGlobalOtel() { OtelReflectionUtils.shutdownAndResetGlobalOtel(); } + public static class NoOpParentOverrideHandler implements BiConsumer { + + @Override + public void accept(SpanBuilder spanBuilder, SpanContext spanContext) {} + } + @Test @DisabledOnOpenJ9 public void checkAllOptions(@TempDir Path tmpDir) { @@ -57,7 +66,10 @@ public void checkAllOptions(@TempDir Path tmpDir) { .put(InferredSpansAutoConfig.EXCLUDED_CLASSES_OPTION, "blub,test*.test2") .put(InferredSpansAutoConfig.INTERVAL_OPTION, "2s") .put(InferredSpansAutoConfig.DURATION_OPTION, "3s") - .put(InferredSpansAutoConfig.LIB_DIRECTORY_OPTION, libDir)) { + .put(InferredSpansAutoConfig.LIB_DIRECTORY_OPTION, libDir) + .put( + InferredSpansAutoConfig.PARENT_OVERRIDE_HANDLER_OPTION, + NoOpParentOverrideHandler.class.getName())) { OpenTelemetry otel = GlobalOpenTelemetry.get(); List processors = OtelReflectionUtils.getSpanProcessors(otel); @@ -81,6 +93,7 @@ public void checkAllOptions(@TempDir Path tmpDir) { assertThat(config.getProfilingInterval()).isEqualTo(Duration.ofSeconds(2)); assertThat(config.getProfilingDuration()).isEqualTo(Duration.ofSeconds(3)); assertThat(config.getProfilerLibDirectory()).isEqualTo(libDir); + assertThat(config.getParentOverrideHandler()).isInstanceOf(NoOpParentOverrideHandler.class); } } diff --git a/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/internal/CallTreeSpanifyTest.java b/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/internal/CallTreeSpanifyTest.java index 302de16e4..8eddda06f 100644 --- a/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/internal/CallTreeSpanifyTest.java +++ b/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/internal/CallTreeSpanifyTest.java @@ -48,7 +48,10 @@ void testSpanification() throws Exception { setup.profiler.setProfilingSessionOngoing(true); CallTree.Root callTree = CallTreeTest.getCallTree(setup, new String[] {" dd ", " cc ", " bbb ", "aaaaee"}); - assertThat(callTree.spanify(nanoClock, setup.sdk.getTracer("dummy-tracer"))).isEqualTo(4); + assertThat( + callTree.spanify( + nanoClock, setup.sdk.getTracer("dummy-tracer"), CallTree.DEFAULT_PARENT_OVERRIDE)) + .isEqualTo(4); assertThat(setup.getSpans()).hasSize(5); assertThat(setup.getSpans().stream().map(SpanData::getName)) .containsExactly( @@ -158,7 +161,8 @@ void testCallTreeWithActiveSpan() { .build()); try (OpenTelemetrySdk outputSdk = sdkBuilder.build()) { - root.spanify(nanoClock, outputSdk.getTracer("dummy-tracer")); + root.spanify( + nanoClock, outputSdk.getTracer("dummy-tracer"), CallTree.DEFAULT_PARENT_OVERRIDE); List spans = exporter.getFinishedSpanItems(); assertThat(spans).hasSize(2); @@ -206,7 +210,8 @@ void testSpanWithInvertedActivation() { .addSpanProcessor(SimpleSpanProcessor.create(exporter)) .build()); try (OpenTelemetrySdk outputSdk = sdkBuilder.build()) { - root.spanify(nanoClock, outputSdk.getTracer("dummy-tracer")); + root.spanify( + nanoClock, outputSdk.getTracer("dummy-tracer"), CallTree.DEFAULT_PARENT_OVERRIDE); List spans = exporter.getFinishedSpanItems(); assertThat(spans).hasSize(1); @@ -249,7 +254,8 @@ void testSpanWithNestedActivation() { .addSpanProcessor(SimpleSpanProcessor.create(exporter)) .build()); try (OpenTelemetrySdk outputSdk = sdkBuilder.build()) { - root.spanify(nanoClock, outputSdk.getTracer("dummy-tracer")); + root.spanify( + nanoClock, outputSdk.getTracer("dummy-tracer"), CallTree.DEFAULT_PARENT_OVERRIDE); List spans = exporter.getFinishedSpanItems(); assertThat(spans).hasSize(1); diff --git a/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/internal/CallTreeTest.java b/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/internal/CallTreeTest.java index 3f23f1e63..8692158a6 100644 --- a/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/internal/CallTreeTest.java +++ b/inferred-spans/src/test/java/io/opentelemetry/contrib/inferredspans/internal/CallTreeTest.java @@ -869,7 +869,10 @@ private Map assertCallTree( assertThat(actualResult).isEqualTo(expectedResult.toString()); if (expectedSpans != null) { - root.spanify(nanoClock, profilerSetup.sdk.getTracer("dummy-inferred-spans-tracer")); + root.spanify( + nanoClock, + profilerSetup.sdk.getTracer("dummy-inferred-spans-tracer"), + CallTree.DEFAULT_PARENT_OVERRIDE); Map spans = profilerSetup.getSpans().stream() .collect(toMap(s -> s.getName().replaceAll(".*#", ""), Function.identity()));