Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
039f678
add property translation for inferred spans
zeitlinger Jul 14, 2025
54bd193
inferred spans
zeitlinger Jul 15, 2025
276565a
inferred spans
zeitlinger Jul 15, 2025
04b1e2b
inferred spans
zeitlinger Jul 15, 2025
c622bd4
format
zeitlinger Jul 15, 2025
00c4eb5
fix
zeitlinger Jul 15, 2025
135cacd
make temp dir more reliable
zeitlinger Jul 15, 2025
f410aac
make temp dir more reliable
zeitlinger Jul 15, 2025
06039b6
test starts async profiler (?)
zeitlinger Jul 15, 2025
c2f1506
Revert "test starts async profiler (?)"
zeitlinger Jul 15, 2025
d91072b
leave profiler disabled in test
zeitlinger Jul 15, 2025
a297c8e
leave profiler disabled in test
zeitlinger Jul 15, 2025
60728e5
copy declarative config from agent
zeitlinger Aug 15, 2025
67d599f
fix
zeitlinger Aug 15, 2025
e49832b
fix package
zeitlinger Aug 18, 2025
2e90880
update file format
zeitlinger Aug 18, 2025
6ccd8ee
add comment
zeitlinger Aug 18, 2025
5873f2e
extract common class
zeitlinger Aug 19, 2025
a5d2d60
add description and owner of the declarative config bridge
zeitlinger Aug 19, 2025
11f5d80
./gradlew spotlessApply
otelbot[bot] Aug 19, 2025
ba16ddf
add description and owner of the declarative config bridge
zeitlinger Aug 19, 2025
05eeaf8
move bridge to agent
zeitlinger Aug 22, 2025
c66298b
rebase
zeitlinger Sep 12, 2025
4ddb381
update
zeitlinger Sep 15, 2025
559f501
update
zeitlinger Sep 15, 2025
eca9228
readme
zeitlinger Sep 15, 2025
6b2f57e
./gradlew spotlessApply
otelbot[bot] Sep 15, 2025
0baba1e
lint
zeitlinger Sep 15, 2025
69a9ba0
fix processor name
zeitlinger Sep 17, 2025
13584b5
./gradlew spotlessApply
otelbot[bot] Sep 17, 2025
775e311
pr review
zeitlinger Oct 30, 2025
614903c
add enabled
zeitlinger Oct 31, 2025
b527dfe
add enabled
zeitlinger Oct 31, 2025
d8faf6e
fix processor name
zeitlinger Nov 3, 2025
dfd975e
fix
zeitlinger Nov 3, 2025
58c4300
fix
zeitlinger Nov 3, 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:
- inferred_spans/development:
enabled: true # true by default unlike autoconfiguration described above
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.createSpanProcessor(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,137 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.inferredspans;

import static java.util.Collections.unmodifiableList;
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 final List<String> ALL_PROPERTIES =
unmodifiableList(
Arrays.asList(
ENABLED_OPTION,
LOGGING_OPTION,
DIAGNOSTIC_FILES_OPTION,
SAFEMODE_OPTION,
POSTPROCESSING_OPTION,
SAMPLING_INTERVAL_OPTION,
MIN_DURATION_OPTION,
INCLUDED_CLASSES_OPTION,
EXCLUDED_CLASSES_OPTION,
INTERVAL_OPTION,
DURATION_OPTION,
LIB_DIRECTORY_OPTION,
PARENT_OVERRIDE_HANDLER_OPTION));

static SpanProcessor createSpanProcessor(ConfigProperties properties) {
InferredSpansProcessorBuilder builder = InferredSpansProcessor.builder().profilerEnabled(true);

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));
}

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