Skip to content

Conversation

@majanjua-amzn
Copy link
Contributor

@majanjua-amzn majanjua-amzn commented Oct 24, 2025

Background

With the addition of onEnding as part of the ExtendedSpanProcessor in OTel core (open-telemetry/opentelemetry-java#6367), we are now able to modify spans before they are sent to an exporter. As such, we have the opportunity to refactor our AwsMetricAttributesSpanExporter which is responsible for adding service and dependency metric related attributes to spans as they are exported. This was done via some complicated span wrapping logic which is no longer necessary with onEnding.

Changes

  • Replaced all references to AwsMetricAttributesSpanExporter with AwsAttributeGeneratingSpanProcessor
  • Refactored AwsMetricAttributesSpanExporter to AwsAttributeGeneratingSpanProcessor, removing any exporter-related functionality and moving the span/attribute modification logic to processor.onEnding()
  • Replaced the AwsMetricAttributesSpanExporterBuilder with the AwsAttributeGeneratingSpanProcessorBuilder
  • Updated relevant tests expecting the exporter to expect the default exporters now that we don't set this extra exporter
  • Rewrote the relevant unit tests, see details in the testing section

Testing

Converted most of the exporter tests into equivalent tests for the new processor:

  • Removed:
    • Tests checking testing delegate classes, export, flush, and shutdown behaviour
      • testPassthroughDelegations
      • testExportDelegatingSpanDataBehaviour
    • Tests checking >1 span being exported at a time, as the processor handles one span at a time:
      • testExportDelegationWithMultipleSpans
    • Helper functions that are no longer needed
  • Added/renamed: (Notice some overlap, test cases before
    • testExportDelegationWithoutAttributeOrModificationtestOnEndingGeneratesBothWithoutOverride
    • testExportDelegationWithAttributeButWithoutModificationtestOnEndingGeneratesService
    • testExportDelegationWithoutAttributeButWithModificationtestOnEndingGeneratesBothWithoutOverride
    • testExportDelegationWithAttributeAndModificationtestOnEndingGeneratesBothWithOverride
    • testOverridenAttributestestOnEndingGeneratesBothWithOverride
    • testExportDelegationWithTwoMetricstestOnEndingGeneratesBothWithTwoMetrics
    • testConsumerProcessSpanHasEmptyAttributetestConsumerSpanHasEmptyAttributes
    • testExportDelegationWithDependencyMetricstestOnEndingGeneratesDependency

Also ran the entire main build workflow using this change successfully:
https://github.com/aws-observability/aws-otel-java-instrumentation/actions/runs/18785649764

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@majanjua-amzn majanjua-amzn self-assigned this Oct 24, 2025
@majanjua-amzn majanjua-amzn added the java Pull requests that update Java code label Oct 24, 2025
@majanjua-amzn majanjua-amzn force-pushed the refactor-exporter branch 3 times, most recently from 7167e8c to 0e1a512 Compare October 24, 2025 21:34
@majanjua-amzn majanjua-amzn marked this pull request as ready for review October 24, 2025 21:40
@majanjua-amzn majanjua-amzn requested a review from a team as a code owner October 24, 2025 21:40
// Construct and set local and remote attributes span processor
tracerProviderBuilder.addSpanProcessor(
AttributePropagatingSpanProcessorBuilder.create().build());
// Construct and set AWS Attribute Generating Span Processor
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe comment that the order of span processors matters. I actually wonder if it would be better to have some kind of explicit enforcement of this.

import io.opentelemetry.sdk.trace.ReadWriteSpan;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.sdk.trace.internal.ExtendedSpanProcessor;
Copy link
Contributor

Choose a reason for hiding this comment

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

I forgot that OnEnding is in development, forcing us to depend on internal components explicitly not for public usage. I just want to take a moment and think about this before committing hard on this. I think fundamentally the functionality we are using is not complex and unlikely to just fundamentally change or be reverted, but development features are explicitly prone to that.

Comment on lines +63 to +67
SpanData spanData = span.toSpanData();
Map<String, Attributes> attributeMap =
generator.generateMetricAttributeMapFromSpan(span.toSpanData(), resource);

boolean generatesServiceMetrics =
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
SpanData spanData = span.toSpanData();
Map<String, Attributes> attributeMap =
generator.generateMetricAttributeMapFromSpan(span.toSpanData(), resource);
boolean generatesServiceMetrics =
SpanData spanData = span.toSpanData();
Map<String, Attributes> attributeMap =
generator.generateMetricAttributeMapFromSpan(spanData, resource);
boolean generatesServiceMetrics =

.thenReturn(attributeMap);

// End span and verify it was modified appropriately
span.end();
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious - how does onEnding get called?

}

@Test
public void testOnEndingGeneratesBothWithoutOverride() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not blocking - a couple comments would not go amiss - e.g. why is this span a local root? What is/isn't being overridden? Both what?

TBH I find the name confusing anyways, so if not a comment, maybe a new name - same for the rest of these tests.


@Test
public void testOnEndingGeneratesService() {
Span span = tracer.spanBuilder("test").setSpanKind(SpanKind.SERVER).startSpan();
Copy link
Contributor

Choose a reason for hiding this comment

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

Have at least two span attribute (one replaced, one unchanged) to show we don't just completely replace attributes, etc. Same with testOnEndingGeneratesDependency

}

@Test
public void testOnEndingGeneratesService() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public void testOnEndingGeneratesService() {
public void testOnEndingGeneratesServiceAttributes() {

etc.

Comment on lines +168 to +181
// Ensure span is treated as only having dependency metric information
try (MockedStatic<AwsSpanProcessingUtil> mockUtil =
Mockito.mockStatic(AwsSpanProcessingUtil.class)) {
mockUtil
.when(
() ->
AwsSpanProcessingUtil.shouldGenerateServiceMetricAttributes(any(SpanData.class)))
.thenReturn(false);
mockUtil
.when(
() ->
AwsSpanProcessingUtil.shouldGenerateDependencyMetricAttributes(
any(SpanData.class)))
.thenReturn(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

This is fine, but you could also probably just have a parent and child span, then audit the child span only.

import org.mockito.MockitoAnnotations;

/** Unit tests for {@link AwsAttributeGeneratingSpanProcessor}. */
class AwsAttributeGeneratingSpanProcessorTest {
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably for all tests: we should include both DEPENDENCY_METRIC and SERVICE_METRIC in attributeMap, to show that only one is ever used. Actually ideally we would do something where if we expect only DEPENDENCY_METRIC to be used, then if we attempt to pull SERVICE_METRIC, we throw an exception and fail the test. Or something.

Comment on lines +72 to +80
if (generatesServiceMetrics && generatesDependencyMetrics) {
// Order matters: dependency metric attributes include AWS_SPAN_KIND key
span.setAllAttributes(attributeMap.get(MetricAttributeGenerator.DEPENDENCY_METRIC));
span.setAttribute(AWS_SPAN_KIND, AwsSpanProcessingUtil.LOCAL_ROOT);
} else if (generatesServiceMetrics) {
span.setAllAttributes(attributeMap.get(MetricAttributeGenerator.SERVICE_METRIC));
} else if (generatesDependencyMetrics) {
span.setAllAttributes(attributeMap.get(MetricAttributeGenerator.DEPENDENCY_METRIC));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Consideration: If (for example) attributeMap does NOT contain SERVICE_METRIC and we call span.setAllAttributes(attributeMap.get(MetricAttributeGenerator.SERVICE_METRIC)), what happens? If it throws an exception or something, then that is a problem, because onEnding is now in application path (where before export was, IIRC, async). We may need to do some defensive programming here and log debugs if expectations are not met.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

java Pull requests that update Java code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants