Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions inferred-spans/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ So if you are using an autoconfigured OpenTelemetry SDK, you'll only need to add
| otel.inferred.spans.interval <br/> OTEL_INFERRED_SPANS_INTERVAL | `5s` | The interval at which profiling sessions should be started. |
| 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` |
| 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. |
| 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` |
| 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 |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if you could use ComponentProvider, which is how file configuration is supporting configuring things like this (instead of fully-qualified class name)

cc @jack-berg

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually this definitely would make sense I think. However, is the new structured config API already in a state so that we can fully replace the usage of the AutoConfigurationCustomizerProvider here? Because I don't think that we can simply mix in the ComponentProvider to my understanding

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense

I sent a PR to your branch just to demonstrate what I was thinking: JonasKunz#1

but it does require depending on a bunch of internal / experimental classes, so really not sure if it's worth it yet

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ComponentProvider is for providing implementations of SDK plugin extension points referenced in the declarative confg files. I.e. samplers, processors, exporters, etc.

@trask it looks like you're using it to provide an implementation of ParentOverrideHandler (not an SDK plugin extension point), and using outside of declarative config.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yeah, I was thinking maybe we could use component provider for custom declarative config stuff, but I guess declarative config has no way to know about what nodes represent components and what provider types to use


### Manual SDK setup

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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(
Expand All @@ -65,6 +76,16 @@ public void customize(AutoConfigurationCustomizer config) {
});
}

@SuppressWarnings("unchecked")
private static BiConsumer<SpanBuilder, SpanContext> constructParentOverrideHandler(String name) {
try {
Class<?> clazz = Class.forName(name);
return (BiConsumer<SpanBuilder, SpanContext>) clazz.getConstructor().newInstance();
} catch (Exception e) {
throw new IllegalArgumentException("Could not construct parent override handler", e);
}
}

private static class PropertiesApplier {

private final ConfigProperties properties;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -48,6 +52,8 @@ public class InferredSpansProcessorBuilder {

@Nullable private File activationEventsFile = null;
@Nullable private File jfrFile = null;
private BiConsumer<SpanBuilder, SpanContext> parentOverrideHandler =
CallTree.DEFAULT_PARENT_OVERRIDE;

InferredSpansProcessorBuilder() {}

Expand All @@ -64,7 +70,8 @@ public InferredSpansProcessor build() {
excludedClasses,
profilerInterval,
profilingDuration,
profilerLibDirectory);
profilerLibDirectory,
parentOverrideHandler);
return new InferredSpansProcessor(
config, clock, startScheduledProfiling, activationEventsFile, jfrFile);
}
Expand Down Expand Up @@ -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<SpanBuilder, SpanContext> handler) {
this.parentOverrideHandler = handler;
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<SpanBuilder, SpanContext> DEFAULT_PARENT_OVERRIDE =
(inferredSpan, child) -> inferredSpan.addLink(child, CHILD_LINK_ATTRIBUTES);

@Nullable private CallTree parent;
protected int count;
private List<CallTree> children = new ArrayList<>(INITIAL_CHILD_SIZE);
Expand Down Expand Up @@ -427,6 +431,7 @@ int spanify(
@Nullable Span parentSpan,
TraceContext parentContext,
SpanAnchoredClock clock,
BiConsumer<SpanBuilder, SpanContext> normalSpanParentOverride,
StringBuilder tempBuilder,
Tracer tracer) {
int createdSpans = 0;
Expand All @@ -437,7 +442,15 @@ int spanify(
Span span = null;
if (!isPillar() || isLeaf()) {
createdSpans++;
span = asSpan(root, parentSpan, parentContext, tracer, clock, tempBuilder);
span =
asSpan(
root,
parentSpan,
parentContext,
tracer,
clock,
normalSpanParentOverride,
tempBuilder);
this.isSpan = true;
}
List<CallTree> children = getChildren();
Expand All @@ -450,6 +463,7 @@ int spanify(
span != null ? span : parentSpan,
parentContext,
clock,
normalSpanParentOverride,
tempBuilder,
tracer);
}
Expand All @@ -462,6 +476,7 @@ protected Span asSpan(
TraceContext parentContext,
Tracer tracer,
SpanAnchoredClock clock,
BiConsumer<SpanBuilder, SpanContext> normalSpanParentOverride,
StringBuilder tempBuilder) {

Context parentOtelCtx;
Expand Down Expand Up @@ -494,7 +509,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,
normalSpanParentOverride,
tempBuilder);

// we're not interested in the very bottom of the stack which contains things like accepting and
// handling connections
Expand All @@ -517,6 +536,7 @@ private void insertChildIdLinks(
SpanBuilder span,
SpanContext parentContext,
TraceContext nonInferredParent,
BiConsumer<SpanBuilder, SpanContext> normalSpanParentOverride,
StringBuilder tempBuilder) {
if (childIds == null || childIds.isEmpty()) {
return;
Expand All @@ -527,13 +547,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);
normalSpanParentOverride.accept(span, childSpanContext);
}
}
}
Expand Down Expand Up @@ -863,13 +883,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<SpanBuilder, SpanContext> normalSpanOverride) {
StringBuilder tempBuilder = new StringBuilder();
int createdSpans = 0;
List<CallTree> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -22,8 +25,8 @@ public class InferredSpansConfiguration {
private final List<WildcardMatcher> excludedClasses;
private final Duration profilerInterval;
private final Duration profilingDuration;

@Nullable private final String profilerLibDirectory;
private final BiConsumer<SpanBuilder, SpanContext> parentOverrideHandler;

@SuppressWarnings("TooManyParameters")
public InferredSpansConfiguration(
Expand All @@ -37,7 +40,8 @@ public InferredSpansConfiguration(
List<WildcardMatcher> excludedClasses,
Duration profilerInterval,
Duration profilingDuration,
@Nullable String profilerLibDirectory) {
@Nullable String profilerLibDirectory,
BiConsumer<SpanBuilder, SpanContext> parentOverrideHandler) {
this.profilerLoggingEnabled = profilerLoggingEnabled;
this.backupDiagnosticFiles = backupDiagnosticFiles;
this.asyncProfilerSafeMode = asyncProfilerSafeMode;
Expand All @@ -49,6 +53,7 @@ public InferredSpansConfiguration(
this.profilerInterval = profilerInterval;
this.profilingDuration = profilingDuration;
this.profilerLibDirectory = profilerLibDirectory;
this.parentOverrideHandler = parentOverrideHandler;
}

public boolean isProfilingLoggingEnabled() {
Expand Down Expand Up @@ -100,4 +105,8 @@ public String getProfilerLibDirectory() {
public boolean isPostProcessingEnabled() {
return postProcessingEnabled;
}

public BiConsumer<SpanBuilder, SpanContext> getParentOverrideHandler() {
return parentOverrideHandler;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -40,6 +43,12 @@ public void resetGlobalOtel() {
OtelReflectionUtils.shutdownAndResetGlobalOtel();
}

public static class NoOpParentOverrideHandler implements BiConsumer<SpanBuilder, SpanContext> {

@Override
public void accept(SpanBuilder spanBuilder, SpanContext spanContext) {}
}

@Test
@DisabledOnOpenJ9
public void checkAllOptions(@TempDir Path tmpDir) {
Expand All @@ -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<SpanProcessor> processors = OtelReflectionUtils.getSpanProcessors(otel);
Expand All @@ -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);
}
}

Expand Down
Loading
Loading