From c31edc6105f8fa6748e79609886a2ff2c13a197c Mon Sep 17 00:00:00 2001 From: cleverchuk Date: Thu, 22 Oct 2026 17:09:12 -0400 Subject: [PATCH 1/2] Internal - Add swo_stacktrace declarative config component with span stacktrace filters --- .../extensions/SpanStacktraceFilter.java | 16 +- .../parser/yaml/StacktraceFilterParser.java | 44 ----- .../SharedConfigCustomizerProvider.java | 42 ----- .../provider/StacktraceComponentProvider.java | 71 ++++++++ .../extensions/SpanStacktraceFilterTest.java | 13 ++ .../yaml/StacktraceFilterParserTest.java | 62 ------- .../SharedConfigCustomizerProviderTest.java | 140 +-------------- .../StacktraceComponentProviderTest.java | 166 ++++++++++++++++++ ...sdk-config-bad-transaction-settings-0.yaml | 2 - ...sdk-config-bad-transaction-settings-1.yaml | 2 - .../shared/src/test/resources/sdk-config.yaml | 2 - sdk-config.yaml | 1 + smoke-tests/sdk-config.yaml | 8 +- 13 files changed, 271 insertions(+), 298 deletions(-) delete mode 100644 libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/parser/yaml/StacktraceFilterParser.java create mode 100644 libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProvider.java delete mode 100644 libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/parser/yaml/StacktraceFilterParserTest.java create mode 100644 libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProviderTest.java diff --git a/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/SpanStacktraceFilter.java b/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/SpanStacktraceFilter.java index b2413620..0e5d7537 100644 --- a/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/SpanStacktraceFilter.java +++ b/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/SpanStacktraceFilter.java @@ -20,6 +20,7 @@ import com.solarwinds.joboe.config.ConfigProperty; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.sdk.trace.ReadableSpan; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -27,17 +28,28 @@ import java.util.stream.Collectors; public class SpanStacktraceFilter implements Predicate { + private static final String DEFAULT_FILTER_ATTRIBUTE = "db.system"; + private Set filterAttributes = new HashSet<>(); + public SpanStacktraceFilter() {} + + public SpanStacktraceFilter(Collection configuredFilterAttributes) { + Set effectiveFilterAttributes = new HashSet<>(configuredFilterAttributes); + effectiveFilterAttributes.add(DEFAULT_FILTER_ATTRIBUTE); + filterAttributes = effectiveFilterAttributes; + } + @Override public boolean test(ReadableSpan readableSpan) { if (filterAttributes.isEmpty()) { Set configuredFilterAttributes = ConfigManager.getConfigOptional( - ConfigProperty.AGENT_SPAN_STACKTRACE_FILTERS, Collections.singleton("db.system")); + ConfigProperty.AGENT_SPAN_STACKTRACE_FILTERS, + Collections.singleton(DEFAULT_FILTER_ATTRIBUTE)); Set effectiveFilterAttributes = new HashSet<>(configuredFilterAttributes); - effectiveFilterAttributes.add("db.system"); + effectiveFilterAttributes.add(DEFAULT_FILTER_ATTRIBUTE); filterAttributes = effectiveFilterAttributes; } diff --git a/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/parser/yaml/StacktraceFilterParser.java b/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/parser/yaml/StacktraceFilterParser.java deleted file mode 100644 index 01529e21..00000000 --- a/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/parser/yaml/StacktraceFilterParser.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * © SolarWinds Worldwide, LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.solarwinds.opentelemetry.extensions.config.parser.yaml; - -import com.google.auto.service.AutoService; -import com.solarwinds.joboe.config.InvalidConfigException; -import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -@AutoService(DeclarativeConfigPropertyParser.class) -public final class StacktraceFilterParser implements DeclarativeConfigPropertyParser { - private static final String CONFIG_KEY = "agent.spanStacktraceFilters"; - - @Override - public Set convert(DeclarativeConfigProperties declarativeConfigProperties) - throws InvalidConfigException { - List spanAttributes = - declarativeConfigProperties.getScalarList( - CONFIG_KEY, String.class, Collections.emptyList()); - return new HashSet<>(spanAttributes); - } - - @Override - public String configKey() { - return CONFIG_KEY; - } -} diff --git a/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/provider/SharedConfigCustomizerProvider.java b/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/provider/SharedConfigCustomizerProvider.java index c51d4e65..cf082e01 100644 --- a/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/provider/SharedConfigCustomizerProvider.java +++ b/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/provider/SharedConfigCustomizerProvider.java @@ -16,8 +16,6 @@ package com.solarwinds.opentelemetry.extensions.config.provider; -import static com.solarwinds.opentelemetry.extensions.SharedNames.SPAN_STACKTRACE_FILTER_CLASS; - import com.google.auto.service.AutoService; import com.solarwinds.joboe.config.ConfigManager; import com.solarwinds.joboe.config.ConfigProperty; @@ -52,7 +50,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; @AutoService(DeclarativeConfigurationCustomizerProvider.class) public class SharedConfigCustomizerProvider implements DeclarativeConfigurationCustomizerProvider { @@ -88,7 +85,6 @@ public void customize(DeclarativeConfigurationCustomizer customizer) { addMetricExporter(configurationModel); if (tracerProvider != null) { addSampler(tracerProvider); - addProcessors(tracerProvider); addSpanExporter(configurationModel); } @@ -110,44 +106,6 @@ private static Map properties(Object... keyValues) { return properties; } - private void addProcessors(TracerProviderModel model) { - List spanProcessorModels = model.getProcessors(); - if (spanProcessorModels == null) { - spanProcessorModels = new ArrayList<>(); - } - - ArrayList allProcessors = new ArrayList<>(spanProcessorModels); - String experimentalStacktrace = "stacktrace/development"; - Optional modelOptional = - spanProcessorModels.stream() - .filter( - processorModel -> - processorModel.getExtensionProperties().containsKey(experimentalStacktrace)) - .findFirst(); - - if (!modelOptional.isPresent()) { - allProcessors.add( - new SpanProcessorModel() - .setExtensionProperty( - experimentalStacktrace, properties("filter", SPAN_STACKTRACE_FILTER_CLASS))); - } else { - SpanProcessorModel spanProcessorModel = modelOptional.get(); - Object existing = spanProcessorModel.getExtensionProperties().get(experimentalStacktrace); - Map stacktraceProperties = new LinkedHashMap<>(); - - if (existing instanceof Map) { - ((Map) existing).forEach((k, v) -> stacktraceProperties.put(String.valueOf(k), v)); - } - - if (!stacktraceProperties.containsKey("filter")) { - stacktraceProperties.put("filter", SPAN_STACKTRACE_FILTER_CLASS); - spanProcessorModel.setExtensionProperty(experimentalStacktrace, stacktraceProperties); - } - } - - model.setProcessors(allProcessors); - } - private void addSampler(TracerProviderModel model) { SamplerModel sampler = model.getSampler(); if (sampler == null) { diff --git a/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProvider.java b/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProvider.java new file mode 100644 index 00000000..397eb075 --- /dev/null +++ b/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProvider.java @@ -0,0 +1,71 @@ +/* + * © SolarWinds Worldwide, LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.solarwinds.opentelemetry.extensions.config.provider; + +import com.google.auto.service.AutoService; +import com.solarwinds.opentelemetry.extensions.SpanStacktraceFilter; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.contrib.stacktrace.StackTraceComponentProvider; +import io.opentelemetry.contrib.stacktrace.StackTraceSpanProcessor; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; +import io.opentelemetry.sdk.trace.SpanProcessor; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Drop-in replacement for {@code stacktrace/development} that defaults the span filter to {@link + * SpanStacktraceFilter}. The span attribute keys that trigger capture are set with {@code filters}; + * {@code db.system} always applies. + */ +@AutoService(ComponentProvider.class) +public class StacktraceComponentProvider implements ComponentProvider { + + public static final String COMPONENT_NAME = "swo_stacktrace"; + + private static final long DEFAULT_MIN_DURATION_MILLIS = 5; + + @Override + public Class getType() { + return SpanProcessor.class; + } + + @Override + public String getName() { + return COMPONENT_NAME; + } + + @Override + public SpanProcessor create(DeclarativeConfigProperties declarativeConfigProperties) { + long minDurationMillis = + declarativeConfigProperties.getLong("min_duration", DEFAULT_MIN_DURATION_MILLIS); + + if (minDurationMillis < 0) { + return SpanProcessor.composite(); + } + + if (declarativeConfigProperties.getString("filter") != null) { + return new StackTraceComponentProvider().create(declarativeConfigProperties); + } + + List filters = + declarativeConfigProperties.getScalarList("filters", String.class, Collections.emptyList()); + SpanStacktraceFilter filter = new SpanStacktraceFilter(filters); + + return new StackTraceSpanProcessor(TimeUnit.MILLISECONDS.toNanos(minDurationMillis), filter); + } +} diff --git a/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/SpanStacktraceFilterTest.java b/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/SpanStacktraceFilterTest.java index b51760b6..a7debb21 100644 --- a/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/SpanStacktraceFilterTest.java +++ b/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/SpanStacktraceFilterTest.java @@ -28,6 +28,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.trace.ReadableSpan; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.junit.jupiter.api.AfterEach; @@ -109,6 +110,18 @@ void returnTrueWhenSpanHasCustomConfiguredLongArrayAttribute() throws InvalidCon assertTrue(filter.test(span)); } + @Test + void explicitFiltersAreUsedWithDbSystemDefault() { + SpanStacktraceFilter filter = new SpanStacktraceFilter(Collections.singletonList("process.id")); + + assertTrue( + filter.test(createSpanWithAttributes(Attributes.builder().put("process.id", 1).build()))); + assertTrue( + filter.test(createSpanWithAttributes(Attributes.builder().put("db.system", "x").build()))); + assertFalse( + filter.test(createSpanWithAttributes(Attributes.builder().put("other", "x").build()))); + } + private ReadableSpan createSpanWithAttributes(Attributes attributes) { ReadableSpan span = mock(ReadableSpan.class); when(span.getAttributes()).thenReturn(attributes); diff --git a/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/parser/yaml/StacktraceFilterParserTest.java b/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/parser/yaml/StacktraceFilterParserTest.java deleted file mode 100644 index c8d0d31a..00000000 --- a/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/parser/yaml/StacktraceFilterParserTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * © SolarWinds Worldwide, LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.solarwinds.opentelemetry.extensions.config.parser.yaml; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import com.solarwinds.joboe.config.InvalidConfigException; -import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; -import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfiguration; -import java.io.IOException; -import java.io.InputStream; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -class StacktraceFilterParserTest { - private static DeclarativeConfigProperties declarativeConfigProperties; - private final StacktraceFilterParser tested = new StacktraceFilterParser(); - - @BeforeAll - static void setup() { - try (InputStream resourceAsStream = - StacktraceFilterParserTest.class.getResourceAsStream("/sdk-config.yaml")) { - DeclarativeConfigProperties configProperties = - DeclarativeConfiguration.toConfigProperties(resourceAsStream); - declarativeConfigProperties = - configProperties - .getStructured("instrumentation/development", DeclarativeConfigProperties.empty()) - .getStructured("java", DeclarativeConfigProperties.empty()) - .getStructured("solarwinds"); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Test - void testConvert() throws InvalidConfigException { - Set attributes = tested.convert(declarativeConfigProperties); - assertEquals(new HashSet<>(Collections.singleton("http.request.method")), attributes); - } - - @Test - void configKey() { - assertEquals("agent.spanStacktraceFilters", tested.configKey()); - } -} diff --git a/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/provider/SharedConfigCustomizerProviderTest.java b/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/provider/SharedConfigCustomizerProviderTest.java index c5508dd6..3a100dbf 100644 --- a/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/provider/SharedConfigCustomizerProviderTest.java +++ b/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/provider/SharedConfigCustomizerProviderTest.java @@ -50,7 +50,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import java.util.function.Function; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -108,7 +107,7 @@ void testCustomize() { SamplerModel sampler = tracerProvider.getSampler(); assertNotNull(sampler.getExtensionProperties().get(SamplerComponentProvider.COMPONENT_NAME)); - assertEquals(2, tracerProvider.getProcessors().size()); + assertEquals(1, tracerProvider.getProcessors().size()); PeriodicMetricReaderModel periodic = openTelemetryConfigurationModel.getMeterProvider().getReaders().get(0).getPeriodic(); @@ -171,7 +170,7 @@ void testCustomize1() { SamplerModel sampler = tracerProvider.getSampler(); assertNotNull(sampler.getExtensionProperties().get(SamplerComponentProvider.COMPONENT_NAME)); - assertEquals(2, tracerProvider.getProcessors().size()); + assertEquals(1, tracerProvider.getProcessors().size()); PeriodicMetricReaderModel periodic = openTelemetryConfigurationModel.getMeterProvider().getReaders().get(0).getPeriodic(); @@ -319,141 +318,6 @@ void customizeShouldNotSetTraceExporterWhenSimpleIsSpecified() { assertNull(spanExporterPropertyModel); } - @Test - void testCustomizeSetsExperimentalStacktraceWhenNotSet() { - OpenTelemetryConfigurationModel openTelemetryConfigurationModel = - withInstrumentation( - new OpenTelemetryConfigurationModel() - .setTracerProvider( - new TracerProviderModel().setProcessors(Collections.emptyList())), - new ExperimentalInstrumentationModel() - .setJava( - new ExperimentalLanguageSpecificInstrumentationModel() - .setAdditionalProperty( - "solarwinds", - new ExperimentalLanguageSpecificInstrumentationPropertyModel()))); - - doNothing() - .when(declarativeConfigurationCustomizerMock) - .addModelCustomizer(functionArgumentCaptor.capture()); - - tested.customize(declarativeConfigurationCustomizerMock); - functionArgumentCaptor.getValue().apply(openTelemetryConfigurationModel); - TracerProviderModel tracerProvider = openTelemetryConfigurationModel.getTracerProvider(); - - assertNotNull(tracerProvider); - assertTrue( - tracerProvider.getProcessors().stream() - .anyMatch( - processorModel -> - processorModel.getExtensionProperties().containsKey("stacktrace/development"))); - } - - @Test - @SuppressWarnings("unchecked") - void testCustomizeSetExperimentalStacktraceFilterWhenNotSet() { - OpenTelemetryConfigurationModel openTelemetryConfigurationModel = - withInstrumentation( - new OpenTelemetryConfigurationModel() - .setTracerProvider( - new TracerProviderModel() - .setProcessors( - Collections.singletonList( - new SpanProcessorModel() - .setExtensionProperty( - "stacktrace/development", new HashMap())))), - new ExperimentalInstrumentationModel() - .setJava( - new ExperimentalLanguageSpecificInstrumentationModel() - .setAdditionalProperty( - "solarwinds", - new ExperimentalLanguageSpecificInstrumentationPropertyModel()))); - - doNothing() - .when(declarativeConfigurationCustomizerMock) - .addModelCustomizer(functionArgumentCaptor.capture()); - - tested.customize(declarativeConfigurationCustomizerMock); - functionArgumentCaptor.getValue().apply(openTelemetryConfigurationModel); - TracerProviderModel tracerProvider = openTelemetryConfigurationModel.getTracerProvider(); - - assertNotNull(tracerProvider); - Optional> map = - tracerProvider.getProcessors().stream() - .filter( - processorModel -> - processorModel.getExtensionProperties().containsKey("stacktrace/development")) - .map( - processorModel -> - asMap(processorModel.getExtensionProperties().get("stacktrace/development"))) - .findFirst(); - - assertTrue(map.isPresent()); - assertNotNull(map.get().get("filter")); - } - - @Test - void testCustomizeSetsStacktraceFilterWhenStacktraceValueIsNotAMap() { - Map stacktrace = customizeWithStacktrace(null); - - assertNotNull(stacktrace.get("filter")); - } - - @Test - void testCustomizeKeepsUserStacktraceFilterAndOtherProperties() { - Map userProperties = new HashMap<>(); - userProperties.put("filter", "com.example.UserFilter"); - userProperties.put("min_duration", 5); - - Map stacktrace = customizeWithStacktrace(userProperties); - - assertEquals("com.example.UserFilter", stacktrace.get("filter")); - assertEquals(5, stacktrace.get("min_duration")); - } - - @Test - void testCustomizeAddsStacktraceFilterAndKeepsOtherProperties() { - Map userProperties = new HashMap<>(); - userProperties.put("min_duration", 5); - - Map stacktrace = customizeWithStacktrace(userProperties); - - assertNotNull(stacktrace.get("filter")); - assertEquals(5, stacktrace.get("min_duration")); - } - - private Map customizeWithStacktrace(Object stacktraceValue) { - OpenTelemetryConfigurationModel model = - withInstrumentation( - new OpenTelemetryConfigurationModel() - .setTracerProvider( - new TracerProviderModel() - .setProcessors( - Collections.singletonList( - new SpanProcessorModel() - .setExtensionProperty( - "stacktrace/development", stacktraceValue)))), - new ExperimentalInstrumentationModel() - .setJava( - new ExperimentalLanguageSpecificInstrumentationModel() - .setAdditionalProperty( - "solarwinds", - new ExperimentalLanguageSpecificInstrumentationPropertyModel()))); - - doNothing() - .when(declarativeConfigurationCustomizerMock) - .addModelCustomizer(functionArgumentCaptor.capture()); - - tested.customize(declarativeConfigurationCustomizerMock); - functionArgumentCaptor.getValue().apply(model); - - return model.getTracerProvider().getProcessors().stream() - .filter(p -> p.getExtensionProperties().containsKey("stacktrace/development")) - .map(p -> asMap(p.getExtensionProperties().get("stacktrace/development"))) - .findFirst() - .orElseThrow(AssertionError::new); - } - @Test void testCustomize2() { OpenTelemetryConfigurationModel openTelemetryConfigurationModel = diff --git a/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProviderTest.java b/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProviderTest.java new file mode 100644 index 00000000..51632cf2 --- /dev/null +++ b/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProviderTest.java @@ -0,0 +1,166 @@ +/* + * © SolarWinds Worldwide, LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.solarwinds.opentelemetry.extensions.config.provider; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.solarwinds.joboe.config.ConfigManager; +import com.solarwinds.joboe.config.ConfigProperty; +import com.solarwinds.joboe.config.InvalidConfigException; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.internal.ExtendedSpanProcessor; +import io.opentelemetry.semconv.CodeAttributes; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class StacktraceComponentProviderTest { + + private final StacktraceComponentProvider tested = new StacktraceComponentProvider(); + + @AfterEach + void tearDown() { + ConfigManager.reset(); + } + + @Test + void testName() { + assertEquals("swo_stacktrace", tested.getName()); + } + + @Test + void negativeMinDurationDisablesStacktraceCapture() { + DeclarativeConfigProperties config = config(-1, null); + ReadWriteSpan span = span(Long.MAX_VALUE, "db.system"); + + SpanProcessor processor = tested.create(config); + assertTrue(!processor.isEndRequired() && !processor.isStartRequired()); + end(processor, span); + + verify(span, never()).setAttribute(eq(CodeAttributes.CODE_STACKTRACE), anyString()); + } + + @Test + void capturesStacktraceForDefaultFilterAttribute() { + ReadWriteSpan span = span(TimeUnit.MILLISECONDS.toNanos(10), "db.system"); + + end(tested.create(config(5, null)), span); + verify(span).setAttribute(eq(CodeAttributes.CODE_STACKTRACE), anyString()); + } + + @Test + void capturesStacktraceForAttributeInFiltersProperty() { + DeclarativeConfigProperties config = config(5, null); + when(config.getScalarList(eq("filters"), eq(String.class), anyList())) + .thenReturn(Collections.singletonList("http.request.method")); + ReadWriteSpan span = span(TimeUnit.MILLISECONDS.toNanos(10), "http.request.method"); + + end(tested.create(config), span); + verify(span).setAttribute(eq(CodeAttributes.CODE_STACKTRACE), anyString()); + } + + @Test + void ignoresLegacyAgentFilterConfig() throws InvalidConfigException { + ConfigManager.setConfig( + ConfigProperty.AGENT_SPAN_STACKTRACE_FILTERS, Collections.singleton("process.pid")); + ReadWriteSpan span = span(TimeUnit.MILLISECONDS.toNanos(10), "process.pid"); + + end(tested.create(config(5, null)), span); + verify(span, never()).setAttribute(eq(CodeAttributes.CODE_STACKTRACE), anyString()); + } + + @Test + void skipsSpansWithoutFilterAttribute() { + ReadWriteSpan span = span(TimeUnit.MILLISECONDS.toNanos(10), "unrelated.attribute"); + + end(tested.create(config(5, null)), span); + verify(span, never()).setAttribute(eq(CodeAttributes.CODE_STACKTRACE), anyString()); + } + + @Test + void skipsSpansShorterThanMinDuration() { + ReadWriteSpan span = span(TimeUnit.MILLISECONDS.toNanos(1), "db.system"); + + end(tested.create(config(5, null)), span); + verify(span, never()).setAttribute(eq(CodeAttributes.CODE_STACKTRACE), anyString()); + } + + @Test + void zeroMinDurationCapturesEveryMatchingSpan() { + ReadWriteSpan span = span(0, "db.system"); + + end(tested.create(config(0, null)), span); + verify(span).setAttribute(eq(CodeAttributes.CODE_STACKTRACE), anyString()); + } + + @Test + void usesUserSuppliedFilterInsteadOfDefault() { + ReadWriteSpan span = span(TimeUnit.MILLISECONDS.toNanos(10), "unrelated.attribute"); + + end(tested.create(config(5, AcceptAllFilter.class.getName())), span); + verify(span).setAttribute(eq(CodeAttributes.CODE_STACKTRACE), anyString()); + } + + public static class AcceptAllFilter implements Predicate { + @Override + public boolean test(ReadableSpan span) { + return true; + } + } + + private static void end(SpanProcessor processor, ReadWriteSpan span) { + if (processor instanceof ExtendedSpanProcessor) { + ((ExtendedSpanProcessor) processor).onEnding(span); + } + } + + private static DeclarativeConfigProperties config(long minDuration, String filter) { + DeclarativeConfigProperties config = mock(DeclarativeConfigProperties.class); + when(config.getLong(eq("min_duration"), anyLong())).thenReturn(minDuration); + + when(config.getString("filter")).thenReturn(filter); + when(config.getScalarList(eq("filters"), eq(String.class), anyList())) + .thenAnswer(i -> i.getArgument(2)); + return config; + } + + private static ReadWriteSpan span(long latencyNanos, String attributeKey) { + ReadWriteSpan span = mock(ReadWriteSpan.class); + when(span.getLatencyNanos()).thenReturn(latencyNanos); + + when(span.getAttributes()).thenReturn(Attributes.of(stringKey(attributeKey), "v")); + when(span.getInstrumentationScopeInfo()).thenReturn(InstrumentationScopeInfo.create("test")); + return span; + } +} diff --git a/libs/shared/src/test/resources/sdk-config-bad-transaction-settings-0.yaml b/libs/shared/src/test/resources/sdk-config-bad-transaction-settings-0.yaml index 6639c184..9b4c9361 100644 --- a/libs/shared/src/test/resources/sdk-config-bad-transaction-settings-0.yaml +++ b/libs/shared/src/test/resources/sdk-config-bad-transaction-settings-0.yaml @@ -32,8 +32,6 @@ instrumentation/development: agent.logging: level: trace agent.tracingMode: enabled - agent.spanStacktraceFilters: - - http.request.method agent.transactionSettings: - tracing: "disabled" diff --git a/libs/shared/src/test/resources/sdk-config-bad-transaction-settings-1.yaml b/libs/shared/src/test/resources/sdk-config-bad-transaction-settings-1.yaml index 1d14403f..4ff7b446 100644 --- a/libs/shared/src/test/resources/sdk-config-bad-transaction-settings-1.yaml +++ b/libs/shared/src/test/resources/sdk-config-bad-transaction-settings-1.yaml @@ -32,8 +32,6 @@ instrumentation/development: agent.logging: level: trace agent.tracingMode: enabled - agent.spanStacktraceFilters: - - http.request.method agent.transactionSettings: - regex: ".*/specialties" diff --git a/libs/shared/src/test/resources/sdk-config.yaml b/libs/shared/src/test/resources/sdk-config.yaml index 1dc9b960..47e56c13 100644 --- a/libs/shared/src/test/resources/sdk-config.yaml +++ b/libs/shared/src/test/resources/sdk-config.yaml @@ -147,8 +147,6 @@ instrumentation/development: agent.logging: level: trace agent.tracingMode: enabled - agent.spanStacktraceFilters: - - http.request.method agent.transactionSettings: - regex: ".*/specialties" diff --git a/sdk-config.yaml b/sdk-config.yaml index b065d4c3..57a3f2d8 100644 --- a/sdk-config.yaml +++ b/sdk-config.yaml @@ -48,6 +48,7 @@ propagator: # Configure tracer provider. tracer_provider: processors: + - swo_stacktrace: - swo_response_time: - swo_profiler: enabled: false diff --git a/smoke-tests/sdk-config.yaml b/smoke-tests/sdk-config.yaml index 311f886b..f8d23e5a 100644 --- a/smoke-tests/sdk-config.yaml +++ b/smoke-tests/sdk-config.yaml @@ -72,8 +72,11 @@ tracer_provider: - com.sun - sun - sunw - - stacktrace/development: + - swo_stacktrace: min_duration: 0 + filters: + - http.request.method + - process.pid - # Configure a batch span processor. simple: # Configure exporter. @@ -179,6 +182,3 @@ instrumentation/development: tracing: "disabled" agent.serviceKey: ${SW_APM_SERVICE_KEY} agent.collector: apm.collector.na-01.st-ssp.solarwinds.com - agent.spanStacktraceFilters: - - http.request.method - - process.pid \ No newline at end of file From 987510d153168037974bdeb645b94cb8ed7f6afa Mon Sep 17 00:00:00 2001 From: cleverchuk Date: Wed, 28 Oct 2026 11:36:10 -0400 Subject: [PATCH 2/2] Internal - rename filters to selectors --- .../extensions/SpanStacktraceFilter.java | 18 +++++++++--------- .../provider/StacktraceComponentProvider.java | 11 ++++++----- .../StacktraceComponentProviderTest.java | 6 +++--- smoke-tests/sdk-config.yaml | 2 +- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/SpanStacktraceFilter.java b/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/SpanStacktraceFilter.java index 0e5d7537..017235ca 100644 --- a/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/SpanStacktraceFilter.java +++ b/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/SpanStacktraceFilter.java @@ -28,29 +28,29 @@ import java.util.stream.Collectors; public class SpanStacktraceFilter implements Predicate { - private static final String DEFAULT_FILTER_ATTRIBUTE = "db.system"; + private static final String DEFAULT_SELECTOR_ATTRIBUTE = "db.system"; - private Set filterAttributes = new HashSet<>(); + private Set selectorAttributes = new HashSet<>(); public SpanStacktraceFilter() {} public SpanStacktraceFilter(Collection configuredFilterAttributes) { Set effectiveFilterAttributes = new HashSet<>(configuredFilterAttributes); - effectiveFilterAttributes.add(DEFAULT_FILTER_ATTRIBUTE); - filterAttributes = effectiveFilterAttributes; + effectiveFilterAttributes.add(DEFAULT_SELECTOR_ATTRIBUTE); + selectorAttributes = effectiveFilterAttributes; } @Override public boolean test(ReadableSpan readableSpan) { - if (filterAttributes.isEmpty()) { + if (selectorAttributes.isEmpty()) { Set configuredFilterAttributes = ConfigManager.getConfigOptional( ConfigProperty.AGENT_SPAN_STACKTRACE_FILTERS, - Collections.singleton(DEFAULT_FILTER_ATTRIBUTE)); + Collections.singleton(DEFAULT_SELECTOR_ATTRIBUTE)); Set effectiveFilterAttributes = new HashSet<>(configuredFilterAttributes); - effectiveFilterAttributes.add(DEFAULT_FILTER_ATTRIBUTE); - filterAttributes = effectiveFilterAttributes; + effectiveFilterAttributes.add(DEFAULT_SELECTOR_ATTRIBUTE); + selectorAttributes = effectiveFilterAttributes; } Set attributes = @@ -58,6 +58,6 @@ public boolean test(ReadableSpan readableSpan) { .map(AttributeKey::getKey) .collect(Collectors.toSet()); - return filterAttributes.stream().anyMatch(attributes::contains); + return selectorAttributes.stream().anyMatch(attributes::contains); } } diff --git a/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProvider.java b/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProvider.java index 397eb075..f455a628 100644 --- a/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProvider.java +++ b/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProvider.java @@ -29,8 +29,8 @@ /** * Drop-in replacement for {@code stacktrace/development} that defaults the span filter to {@link - * SpanStacktraceFilter}. The span attribute keys that trigger capture are set with {@code filters}; - * {@code db.system} always applies. + * SpanStacktraceFilter}. The span attribute keys that trigger capture are set with {@code + * selectors}; {@code db.system} always applies. */ @AutoService(ComponentProvider.class) public class StacktraceComponentProvider implements ComponentProvider { @@ -62,9 +62,10 @@ public SpanProcessor create(DeclarativeConfigProperties declarativeConfigPropert return new StackTraceComponentProvider().create(declarativeConfigProperties); } - List filters = - declarativeConfigProperties.getScalarList("filters", String.class, Collections.emptyList()); - SpanStacktraceFilter filter = new SpanStacktraceFilter(filters); + List selectors = + declarativeConfigProperties.getScalarList( + "selectors", String.class, Collections.emptyList()); + SpanStacktraceFilter filter = new SpanStacktraceFilter(selectors); return new StackTraceSpanProcessor(TimeUnit.MILLISECONDS.toNanos(minDurationMillis), filter); } diff --git a/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProviderTest.java b/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProviderTest.java index 51632cf2..c3ff9ff0 100644 --- a/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProviderTest.java +++ b/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/config/provider/StacktraceComponentProviderTest.java @@ -80,9 +80,9 @@ void capturesStacktraceForDefaultFilterAttribute() { } @Test - void capturesStacktraceForAttributeInFiltersProperty() { + void capturesStacktraceForAttributeInSelectorsProperty() { DeclarativeConfigProperties config = config(5, null); - when(config.getScalarList(eq("filters"), eq(String.class), anyList())) + when(config.getScalarList(eq("selectors"), eq(String.class), anyList())) .thenReturn(Collections.singletonList("http.request.method")); ReadWriteSpan span = span(TimeUnit.MILLISECONDS.toNanos(10), "http.request.method"); @@ -150,7 +150,7 @@ private static DeclarativeConfigProperties config(long minDuration, String filte when(config.getLong(eq("min_duration"), anyLong())).thenReturn(minDuration); when(config.getString("filter")).thenReturn(filter); - when(config.getScalarList(eq("filters"), eq(String.class), anyList())) + when(config.getScalarList(eq("selectors"), eq(String.class), anyList())) .thenAnswer(i -> i.getArgument(2)); return config; } diff --git a/smoke-tests/sdk-config.yaml b/smoke-tests/sdk-config.yaml index f8d23e5a..85ff14e3 100644 --- a/smoke-tests/sdk-config.yaml +++ b/smoke-tests/sdk-config.yaml @@ -74,7 +74,7 @@ tracer_provider: - sunw - swo_stacktrace: min_duration: 0 - filters: + selectors: - http.request.method - process.pid - # Configure a batch span processor.