Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
08eeaaa
add property translation for inferred spans
zeitlinger Jul 14, 2025
3ddafb0
inferred spans
zeitlinger Jul 15, 2025
d41f6d3
inferred spans
zeitlinger Jul 15, 2025
2ab3fef
inferred spans
zeitlinger Jul 15, 2025
dca6340
format
zeitlinger Jul 15, 2025
8f3a4dc
fix
zeitlinger Jul 15, 2025
f35ba97
make temp dir more reliable
zeitlinger Jul 15, 2025
b7c1d0d
make temp dir more reliable
zeitlinger Jul 15, 2025
5413083
test starts async profiler (?)
zeitlinger Jul 15, 2025
e79481b
Revert "test starts async profiler (?)"
zeitlinger Jul 15, 2025
0767556
leave profiler disabled in test
zeitlinger Jul 15, 2025
29a273c
leave profiler disabled in test
zeitlinger Jul 15, 2025
ae2b1d4
copy declarative config from agent
zeitlinger Aug 15, 2025
5c79303
fix
zeitlinger Aug 15, 2025
2da2282
fix package
zeitlinger Aug 18, 2025
a503d70
update file format
zeitlinger Aug 18, 2025
ef40ca4
add comment
zeitlinger Aug 18, 2025
2ec0b5b
extract common class
zeitlinger Aug 19, 2025
6139d37
add description and owner of the declarative config bridge
zeitlinger Aug 19, 2025
c6036d1
./gradlew spotlessApply
otelbot[bot] Aug 19, 2025
9030a2b
add description and owner of the declarative config bridge
zeitlinger Aug 19, 2025
2cb6a5c
move bridge to agent
zeitlinger Aug 22, 2025
292e664
rebase
zeitlinger Sep 12, 2025
f2b9a37
update
zeitlinger Sep 15, 2025
81eb8a6
update
zeitlinger Sep 15, 2025
2c32733
readme
zeitlinger Sep 15, 2025
ef66353
./gradlew spotlessApply
otelbot[bot] Sep 15, 2025
4c26526
lint
zeitlinger Sep 15, 2025
168161c
fix processor name
zeitlinger Sep 17, 2025
abb0093
./gradlew spotlessApply
otelbot[bot] Sep 17, 2025
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
25 changes: 25 additions & 0 deletions inferred-spans/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,31 @@ So if you are using an autoconfigured OpenTelemetry SDK, you'll only need to add
| 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.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 |

### Usage with declarative configuration

You can configure the inferred spans processor using declarative YAML configuration with the
OpenTelemetry SDK. For example:

```yaml
file_format: 1.0-rc.1
tracer_provider:
processors:
- experimental_inferred_spans:
enabled: true
sampling_interval: 25ms
included_classes: "org.example.myapp.*"
excluded_classes: "java.*"
min_duration: 10ms
interval: 5s
duration: 5s
lib_directory: "/var/tmp"
parent_override_handler: "com.example.MyParentOverrideHandler"
```

All the same settings as for [autoconfiguration](#autoconfiguration) can be used here,
just with the `otel.inferred.spans.` prefix stripped.
For example, `otel.inferred.spans.sampling.interval` becomes `sampling_interval` in YAML.

### Manual SDK setup

If you manually set-up your `OpenTelemetrySDK`, you need to create and register an `InferredSpansProcessor` with your `TracerProvider`:
Expand Down
6 changes: 5 additions & 1 deletion inferred-spans/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ dependencies {
annotationProcessor("com.google.auto.service:auto-service")
compileOnly("com.google.auto.service:auto-service-annotations")
compileOnly("io.opentelemetry:opentelemetry-sdk")
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator")
compileOnly("io.opentelemetry.instrumentation:opentelemetry-declarative-config-bridge")
compileOnly("io.opentelemetry.semconv:opentelemetry-semconv")
implementation("com.lmax:disruptor")
implementation("org.jctools:jctools-core")
Expand All @@ -25,9 +27,11 @@ dependencies {
testImplementation("io.opentelemetry.semconv:opentelemetry-semconv")
testImplementation("io.opentelemetry:opentelemetry-sdk")
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator")
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
testImplementation("io.opentelemetry:opentelemetry-api-incubator")
testImplementation("io.opentelemetry:opentelemetry-exporter-logging")
testImplementation("io.opentelemetry.instrumentation:opentelemetry-declarative-config-bridge")
}

tasks {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,130 +5,29 @@

package io.opentelemetry.contrib.inferredspans;

import static java.util.stream.Collectors.toList;
import static io.opentelemetry.contrib.inferredspans.InferredSpansConfig.ENABLED_OPTION;

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 javax.annotation.Nullable;

@AutoService(AutoConfigurationCustomizerProvider.class)
public class InferredSpansAutoConfig implements AutoConfigurationCustomizerProvider {

private static final Logger log = Logger.getLogger(InferredSpansAutoConfig.class.getName());

static final String ENABLED_OPTION = "otel.inferred.spans.enabled";
static final String LOGGING_OPTION = "otel.inferred.spans.logging.enabled";
static final String DIAGNOSTIC_FILES_OPTION = "otel.inferred.spans.backup.diagnostic.files";
static final String SAFEMODE_OPTION = "otel.inferred.spans.safe.mode";
static final String POSTPROCESSING_OPTION = "otel.inferred.spans.post.processing.enabled";
static final String SAMPLING_INTERVAL_OPTION = "otel.inferred.spans.sampling.interval";
static final String MIN_DURATION_OPTION = "otel.inferred.spans.min.duration";
static final String INCLUDED_CLASSES_OPTION = "otel.inferred.spans.included.classes";
static final String EXCLUDED_CLASSES_OPTION = "otel.inferred.spans.excluded.classes";
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) {
config.addTracerProviderCustomizer(
(providerBuilder, properties) -> {
if (properties.getBoolean(ENABLED_OPTION, false)) {
InferredSpansProcessorBuilder builder = InferredSpansProcessor.builder();

PropertiesApplier applier = new PropertiesApplier(properties);

applier.applyBool(LOGGING_OPTION, builder::profilerLoggingEnabled);
applier.applyBool(DIAGNOSTIC_FILES_OPTION, builder::backupDiagnosticFiles);
applier.applyInt(SAFEMODE_OPTION, builder::asyncProfilerSafeMode);
applier.applyBool(POSTPROCESSING_OPTION, builder::postProcessingEnabled);
applier.applyDuration(SAMPLING_INTERVAL_OPTION, builder::samplingInterval);
applier.applyDuration(MIN_DURATION_OPTION, builder::inferredSpansMinDuration);
applier.applyWildcards(INCLUDED_CLASSES_OPTION, builder::includedClasses);
applier.applyWildcards(EXCLUDED_CLASSES_OPTION, builder::excludedClasses);
applier.applyDuration(INTERVAL_OPTION, builder::profilerInterval);
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());
providerBuilder.addSpanProcessor(InferredSpansConfig.create(properties));
} else {
log.finest(
"Not enabling inferred spans processor because " + ENABLED_OPTION + " is not set");
}
return providerBuilder;
});
}

@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;

PropertiesApplier(ConfigProperties properties) {
this.properties = properties;
}

void applyBool(String configKey, Consumer<Boolean> funcToApply) {
applyValue(properties.getBoolean(configKey), funcToApply);
}

void applyInt(String configKey, Consumer<Integer> funcToApply) {
applyValue(properties.getInt(configKey), funcToApply);
}

void applyDuration(String configKey, Consumer<Duration> funcToApply) {
applyValue(properties.getDuration(configKey), funcToApply);
}

void applyString(String configKey, Consumer<String> funcToApply) {
applyValue(properties.getString(configKey), funcToApply);
}

void applyWildcards(String configKey, Consumer<? super List<WildcardMatcher>> funcToApply) {
String wildcardListString = properties.getString(configKey);
if (wildcardListString != null && !wildcardListString.isEmpty()) {
List<WildcardMatcher> values =
Arrays.stream(wildcardListString.split(","))
.filter(str -> !str.isEmpty())
.map(WildcardMatcher::valueOf)
.collect(toList());
if (!values.isEmpty()) {
funcToApply.accept(values);
}
}
}

private static <T> void applyValue(@Nullable T value, Consumer<T> funcToApply) {
if (value != null) {
funcToApply.accept(value);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.inferredspans;

import com.google.auto.service.AutoService;
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
import io.opentelemetry.instrumentation.config.bridge.DeclarativeConfigPropertiesBridgeBuilder;
import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider;
import io.opentelemetry.sdk.trace.SpanProcessor;

@SuppressWarnings("rawtypes")
@AutoService(ComponentProvider.class)
public class InferredSpansComponentProvider implements ComponentProvider<SpanProcessor> {

@Override
public String getName() {
return "experimental_inferred_spans";
}

@Override
public SpanProcessor create(DeclarativeConfigProperties config) {
return InferredSpansConfig.create(
new DeclarativeConfigPropertiesBridgeBuilder()
// crop the prefix, because the properties are under the "experimental_inferred_spans"
// processor
.addMapping("otel.inferred.spans.", "")
.build(config));
}

@Override
public Class<SpanProcessor> getType() {
return SpanProcessor.class;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.inferredspans;

import static java.util.stream.Collectors.toList;

import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.trace.SpanProcessor;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import javax.annotation.Nullable;

class InferredSpansConfig {

private InferredSpansConfig() {}

static final String ENABLED_OPTION = "otel.inferred.spans.enabled";
static final String LOGGING_OPTION = "otel.inferred.spans.logging.enabled";
static final String DIAGNOSTIC_FILES_OPTION = "otel.inferred.spans.backup.diagnostic.files";
static final String SAFEMODE_OPTION = "otel.inferred.spans.safe.mode";
static final String POSTPROCESSING_OPTION = "otel.inferred.spans.post.processing.enabled";
static final String SAMPLING_INTERVAL_OPTION = "otel.inferred.spans.sampling.interval";
static final String MIN_DURATION_OPTION = "otel.inferred.spans.min.duration";
static final String INCLUDED_CLASSES_OPTION = "otel.inferred.spans.included.classes";
static final String EXCLUDED_CLASSES_OPTION = "otel.inferred.spans.excluded.classes";
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";

static SpanProcessor create(ConfigProperties properties) {
InferredSpansProcessorBuilder builder = InferredSpansProcessor.builder();

PropertiesApplier applier = new PropertiesApplier(properties);

applier.applyBool(ENABLED_OPTION, builder::profilerEnabled);
applier.applyBool(LOGGING_OPTION, builder::profilerLoggingEnabled);
applier.applyBool(DIAGNOSTIC_FILES_OPTION, builder::backupDiagnosticFiles);
applier.applyInt(SAFEMODE_OPTION, builder::asyncProfilerSafeMode);
applier.applyBool(POSTPROCESSING_OPTION, builder::postProcessingEnabled);
applier.applyDuration(SAMPLING_INTERVAL_OPTION, builder::samplingInterval);
applier.applyDuration(MIN_DURATION_OPTION, builder::inferredSpansMinDuration);
applier.applyWildcards(INCLUDED_CLASSES_OPTION, builder::includedClasses);
applier.applyWildcards(EXCLUDED_CLASSES_OPTION, builder::excludedClasses);
applier.applyDuration(INTERVAL_OPTION, builder::profilerInterval);
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));
}

return builder.build();
}

@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;

PropertiesApplier(ConfigProperties properties) {
this.properties = properties;
}

void applyBool(String configKey, Consumer<Boolean> funcToApply) {
applyValue(properties.getBoolean(configKey), funcToApply);
}

void applyInt(String configKey, Consumer<Integer> funcToApply) {
applyValue(properties.getInt(configKey), funcToApply);
}

void applyDuration(String configKey, Consumer<Duration> funcToApply) {
applyValue(properties.getDuration(configKey), funcToApply);
}

void applyString(String configKey, Consumer<String> funcToApply) {
applyValue(properties.getString(configKey), funcToApply);
}

void applyWildcards(String configKey, Consumer<? super List<WildcardMatcher>> funcToApply) {
String wildcardListString = properties.getString(configKey);
if (wildcardListString != null && !wildcardListString.isEmpty()) {
List<WildcardMatcher> values =
Arrays.stream(wildcardListString.split(","))
.filter(str -> !str.isEmpty())
.map(WildcardMatcher::valueOf)
.collect(toList());
if (!values.isEmpty()) {
funcToApply.accept(values);
}
}
}

private static <T> void applyValue(@Nullable T value, Consumer<T> funcToApply) {
if (value != null) {
funcToApply.accept(value);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ public class InferredSpansProcessor implements SpanProcessor {
@Nullable File activationEventsFile,
@Nullable File jfrFile) {
this.config = config;
profiler = new SamplingProfiler(config, clock, this::getTracer, activationEventsFile, jfrFile);
profiler =
new SamplingProfiler(config, clock, this::getTracer, activationEventsFile, jfrFile, null);
if (startScheduledProfiling) {
profiler.start();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

@SuppressWarnings("CanIgnoreReturnValueSuggester")
public class InferredSpansProcessorBuilder {
private boolean enabled = true;
private boolean profilerLoggingEnabled = true;
private boolean backupDiagnosticFiles = false;
private int asyncProfilerSafeMode = 0;
Expand Down Expand Up @@ -59,6 +60,7 @@ public class InferredSpansProcessorBuilder {
public InferredSpansProcessor build() {
InferredSpansConfiguration config =
new InferredSpansConfiguration(
enabled,
profilerLoggingEnabled,
backupDiagnosticFiles,
asyncProfilerSafeMode,
Expand All @@ -78,6 +80,11 @@ public InferredSpansProcessor build() {
return processor;
}

public InferredSpansProcessorBuilder profilerEnabled(boolean profilerEnabled) {
this.enabled = profilerEnabled;
return this;
}

/**
* By default, async profiler prints warning messages about missing JVM symbols to standard
* output. Set this option to {@code false} to suppress such messages.
Expand Down
Loading
Loading