diff --git a/CHANGELOG.md b/CHANGELOG.md index b5b3250443..0a5f527175 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,3 +21,7 @@ If your change does not need a CHANGELOG entry, add the "skip changelog" label t ([#1201](https://github.com/aws-observability/aws-otel-java-instrumentation/pull/1201)) - Add support for new formal database semantic convention keys. ([#1162](https://github.com/aws-observability/aws-otel-java-instrumentation/pull/1162)) + +### Refactors +- Refactor AwsMetricAttributesSpanExporter to an onEnding processor + ([#1250](https://github.com/aws-observability/aws-otel-java-instrumentation/pull/1250)) diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/httpservers/base/BaseHttpServerTest.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/httpservers/base/BaseHttpServerTest.java index ce55c23fe5..3fdba0e3a4 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/httpservers/base/BaseHttpServerTest.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/httpservers/base/BaseHttpServerTest.java @@ -52,7 +52,7 @@ public abstract class BaseHttpServerTest extends ContractTestBase { /** - * Assert span attributes inserted by the AwsMetricAttributesSpanExporter + * Assert span attributes inserted by the AwsAttributeGeneratingSpanProcessor * * @param resourceScopeSpans list of spans that were exported by the application * @param method the http method that was used (GET, PUT, DELETE...) diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java index 1073b52bc3..a066954e40 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java @@ -76,7 +76,7 @@ *
  • Add SpanMetricsProcessor to create RED metrics. *
  • Add AttributePropagatingSpanProcessor to propagate span attributes from parent to child * spans. - *
  • Add AwsMetricAttributesSpanExporter to add more attributes to all spans. + *
  • Add AwsAttributeGeneratingSpanProcessor to add more attributes to all spans. * * *

    You can control when these customizations are applied using the property @@ -337,6 +337,9 @@ private SdkTracerProviderBuilder customizeTracerProviderBuilder( // Construct and set local and remote attributes span processor tracerProviderBuilder.addSpanProcessor( AttributePropagatingSpanProcessorBuilder.create().build()); + // Construct and set AWS Attribute Generating Span Processor + tracerProviderBuilder.addSpanProcessor( + AwsAttributeGeneratingSpanProcessorBuilder.create(ResourceHolder.getResource()).build()); // If running on Lambda, we just need to export 100% spans and skip generating any Application // Signals metrics. @@ -351,16 +354,9 @@ private SdkTracerProviderBuilder customizeTracerProviderBuilder( .setEndpoint(tracesEndpoint) .build(); - // Wrap the udp exporter with the AwsMetricsAttributesSpanExporter to add Application - // Signals attributes to unsampled spans too - SpanExporter appSignalsSpanExporter = - AwsMetricAttributesSpanExporterBuilder.create( - spanExporter, ResourceHolder.getResource()) - .build(); - tracerProviderBuilder.addSpanProcessor( AwsUnsampledOnlySpanProcessorBuilder.create() - .setSpanExporter(appSignalsSpanExporter) + .setSpanExporter(spanExporter) .setMaxExportBatchSize(LAMBDA_SPAN_EXPORT_BATCH_SIZE) .build()); return tracerProviderBuilder; @@ -461,12 +457,6 @@ SpanExporter customizeSpanExporter(SpanExporter spanExporter, ConfigProperties c } } - if (isApplicationSignalsEnabled(configProps)) { - spanExporter = - AwsMetricAttributesSpanExporterBuilder.create(spanExporter, ResourceHolder.getResource()) - .build(); - } - if (this.sampler instanceof AwsXrayRemoteSampler) { ((AwsXrayRemoteSampler) this.sampler).setSpanExporter(spanExporter); } diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeGeneratingSpanProcessor.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeGeneratingSpanProcessor.java new file mode 100644 index 0000000000..2c817a6267 --- /dev/null +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeGeneratingSpanProcessor.java @@ -0,0 +1,95 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.opentelemetry.javaagent.providers; + +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_SPAN_KIND; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.resources.Resource; +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; +import java.util.Map; + +public class AwsAttributeGeneratingSpanProcessor implements ExtendedSpanProcessor { + + private final MetricAttributeGenerator generator; + private final Resource resource; + + public static AwsAttributeGeneratingSpanProcessor create( + MetricAttributeGenerator generator, Resource resource) { + return new AwsAttributeGeneratingSpanProcessor(generator, resource); + } + + private AwsAttributeGeneratingSpanProcessor( + MetricAttributeGenerator generator, Resource resource) { + this.generator = generator; + this.resource = resource; + } + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) {} + + @Override + public boolean isStartRequired() { + return false; + } + + @Override + public void onEnding(ReadWriteSpan span) { + // If the map has no items, no modifications are required. If there is one item, it means the + // span either produces Service or Dependency metric attributes, and in either case we want to + // modify the span with them. If there are two items, the span produces both Service and + // Dependency metric attributes indicating the span is a local dependency root. The Service + // Attributes must be a subset of the Dependency, with the exception of AWS_SPAN_KIND. The + // knowledge that the span is a local root is more important than knowing that it is a + // Dependency metric, so we take all the Dependency metrics but replace AWS_SPAN_KIND with + // LOCAL_ROOT. + SpanData spanData = span.toSpanData(); + Map attributeMap = + generator.generateMetricAttributeMapFromSpan(span.toSpanData(), resource); + + boolean generatesServiceMetrics = + AwsSpanProcessingUtil.shouldGenerateServiceMetricAttributes(spanData); + boolean generatesDependencyMetrics = + AwsSpanProcessingUtil.shouldGenerateDependencyMetricAttributes(spanData); + + 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)); + } + } + + @Override + public boolean isOnEndingRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan span) {} + + @Override + public boolean isEndRequired() { + return false; + } +} diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributesSpanExporterBuilder.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeGeneratingSpanProcessorBuilder.java similarity index 62% rename from awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributesSpanExporterBuilder.java rename to awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeGeneratingSpanProcessorBuilder.java index 7de8b00f2a..c5670a2b7e 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributesSpanExporterBuilder.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeGeneratingSpanProcessorBuilder.java @@ -19,43 +19,40 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.trace.export.SpanExporter; -public class AwsMetricAttributesSpanExporterBuilder { +public class AwsAttributeGeneratingSpanProcessorBuilder { // Defaults private static final MetricAttributeGenerator DEFAULT_GENERATOR = new AwsMetricAttributeGenerator(); // Required builder elements - private final SpanExporter delegate; private final Resource resource; // Optional builder elements private MetricAttributeGenerator generator = DEFAULT_GENERATOR; - public static AwsMetricAttributesSpanExporterBuilder create( - SpanExporter delegate, Resource resource) { - return new AwsMetricAttributesSpanExporterBuilder(delegate, resource); + public static AwsAttributeGeneratingSpanProcessorBuilder create(Resource resource) { + return new AwsAttributeGeneratingSpanProcessorBuilder(resource); } - private AwsMetricAttributesSpanExporterBuilder(SpanExporter delegate, Resource resource) { - this.delegate = delegate; + private AwsAttributeGeneratingSpanProcessorBuilder(Resource resource) { this.resource = resource; } /** - * Sets the generator used to generate attributes used spancs exported by the exporter. If unset, + * Sets the generator used to generate attributes added to spans in the processor. If unset, * defaults to {@link #DEFAULT_GENERATOR}. Must not be null. */ @CanIgnoreReturnValue - public AwsMetricAttributesSpanExporterBuilder setGenerator(MetricAttributeGenerator generator) { + public AwsAttributeGeneratingSpanProcessorBuilder setGenerator( + MetricAttributeGenerator generator) { requireNonNull(generator, "generator"); this.generator = generator; return this; } - public AwsMetricAttributesSpanExporter build() { - return AwsMetricAttributesSpanExporter.create(delegate, generator, resource); + public AwsAttributeGeneratingSpanProcessor build() { + return AwsAttributeGeneratingSpanProcessor.create(generator, resource); } } diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributesSpanExporter.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributesSpanExporter.java deleted file mode 100644 index 0ff3e93c34..0000000000 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributesSpanExporter.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file 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 software.amazon.opentelemetry.javaagent.providers; - -import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_SPAN_KIND; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.trace.data.DelegatingSpanData; -import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.sdk.trace.export.SpanExporter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import javax.annotation.concurrent.Immutable; - -/** - * This exporter will update a span with metric attributes before exporting. It depends on a {@link - * SpanExporter} being provided on instantiation, which the AwsSpanMetricsExporter will delegate - * export to. Also, a {@link MetricAttributeGenerator} must be provided, which will provide a means - * to determine attributes which should be applied to the span. Finally, a {@link Resource} must be - * provided, which is used to generate metric attributes. - * - *

    This exporter should be coupled with the {@link AwsSpanMetricsProcessor} using the same {@link - * MetricAttributeGenerator}. This will result in metrics and spans being produced with common - * attributes. - */ -@Immutable -public class AwsMetricAttributesSpanExporter implements SpanExporter { - - private final SpanExporter delegate; - private final MetricAttributeGenerator generator; - private final Resource resource; - - /** Use {@link AwsMetricAttributesSpanExporterBuilder} to construct this exporter. */ - static AwsMetricAttributesSpanExporter create( - SpanExporter delegate, MetricAttributeGenerator generator, Resource resource) { - return new AwsMetricAttributesSpanExporter(delegate, generator, resource); - } - - private AwsMetricAttributesSpanExporter( - SpanExporter delegate, MetricAttributeGenerator generator, Resource resource) { - this.delegate = delegate; - this.generator = generator; - this.resource = resource; - } - - @Override - public CompletableResultCode export(Collection spans) { - List modifiedSpans = addMetricAttributes(spans); - return delegate.export(modifiedSpans); - } - - @Override - public CompletableResultCode flush() { - return delegate.flush(); - } - - @Override - public CompletableResultCode shutdown() { - return delegate.shutdown(); - } - - @Override - public void close() { - delegate.close(); - } - - private List addMetricAttributes(Collection spans) { - List modifiedSpans = new ArrayList<>(); - - for (SpanData span : spans) { - // If the map has no items, no modifications are required. If there is one item, it means the - // span either produces Service or Dependency metric attributes, and in either case we want to - // modify the span with them. If there are two items, the span produces both Service and - // Dependency metric attributes indicating the span is a local dependency root. The Service - // Attributes must be a subset of the Dependency, with the exception of AWS_SPAN_KIND. The - // knowledge that the span is a local root is more important that knowing that it is a - // Dependency metric, so we take all the Dependency metrics but replace AWS_SPAN_KIND with - // LOCAL_ROOT. - Map attributeMap = - generator.generateMetricAttributeMapFromSpan(span, resource); - Attributes attributes = Attributes.empty(); - - boolean generatesServiceMetrics = - AwsSpanProcessingUtil.shouldGenerateServiceMetricAttributes(span); - boolean generatesDependencyMetrics = - AwsSpanProcessingUtil.shouldGenerateDependencyMetricAttributes(span); - - if (generatesServiceMetrics && generatesDependencyMetrics) { - attributes = - copyAttributesWithLocalRoot( - attributeMap.get(MetricAttributeGenerator.DEPENDENCY_METRIC)); - } else if (generatesServiceMetrics) { - attributes = attributeMap.get(MetricAttributeGenerator.SERVICE_METRIC); - } else if (generatesDependencyMetrics) { - attributes = attributeMap.get(MetricAttributeGenerator.DEPENDENCY_METRIC); - } - - if (!attributes.isEmpty()) { - span = wrapSpanWithAttributes(span, attributes); - } - modifiedSpans.add(span); - } - - return modifiedSpans; - } - - private Attributes copyAttributesWithLocalRoot(Attributes attributes) { - AttributesBuilder builder = attributes.toBuilder(); - builder.remove(AWS_SPAN_KIND); - builder.put(AWS_SPAN_KIND, AwsSpanProcessingUtil.LOCAL_ROOT); - return builder.build(); - } - - /** - * {@link #export} works with a {@link SpanData}, which does not permit modification. However, we - * need to add derived metric attributes to the span. To work around this, we will wrap the - * SpanData with a {@link DelegatingSpanData} that simply passes through all API calls, except for - * those pertaining to Attributes, i.e. {@link SpanData#getAttributes()} and {@link - * SpanData#getTotalAttributeCount} APIs. - * - *

    See https://github.com/open-telemetry/opentelemetry-specification/issues/1089 for more - * context on this approach. - */ - private static SpanData wrapSpanWithAttributes(SpanData span, Attributes attributes) { - Attributes originalAttributes = span.getAttributes(); - Attributes replacementAttributes = originalAttributes.toBuilder().putAll(attributes).build(); - - int newAttributeKeyCount = 0; - for (Entry, Object> entry : attributes.asMap().entrySet()) { - if (originalAttributes.get(entry.getKey()) == null) { - newAttributeKeyCount++; - } - } - int originalTotalAttributeCount = span.getTotalAttributeCount(); - int replacementTotalAttributeCount = originalTotalAttributeCount + newAttributeKeyCount; - - return new DelegatingSpanData(span) { - @Override - public Attributes getAttributes() { - return replacementAttributes; - } - - @Override - public int getTotalAttributeCount() { - return replacementTotalAttributeCount; - } - }; - } -} diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/MetricAttributeGenerator.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/MetricAttributeGenerator.java index cb3a5f7cae..d94d73c9de 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/MetricAttributeGenerator.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/MetricAttributeGenerator.java @@ -23,7 +23,7 @@ /** * Metric attribute generator defines an interface for classes that can generate specific attributes * to be used by an {@link AwsSpanMetricsProcessor} to produce metrics and by {@link - * AwsMetricAttributesSpanExporter} to wrap the original span. + * AwsAttributeGeneratingSpanProcessor} to update the original span. */ public interface MetricAttributeGenerator { static final String SERVICE_METRIC = "Service"; diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerTest.java index 1732c95627..0f1d9663b8 100644 --- a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerTest.java +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerTest.java @@ -195,7 +195,7 @@ void testEnableApplicationSignalsSpanExporter() { "otlp"), defaultHttpSpanExporter, this.provider::customizeSpanExporter, - AwsMetricAttributesSpanExporter.class); + OtlpHttpSpanExporter.class); } @Test @@ -212,7 +212,7 @@ void testSigv4ShouldNotDisableApplicationSignalsSpanExporter() { "otlp"), defaultHttpSpanExporter, this.provider::customizeSpanExporter, - AwsMetricAttributesSpanExporter.class); + OtlpAwsSpanExporter.class); } private static void customizeExporterTest( diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeGeneratingSpanProcessorTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeGeneratingSpanProcessorTest.java new file mode 100644 index 0000000000..29d2f918ae --- /dev/null +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeGeneratingSpanProcessorTest.java @@ -0,0 +1,258 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.opentelemetry.javaagent.providers; + +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MessagingOperationTypeIncubatingValues.PROCESS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LOCAL_SERVICE; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_SPAN_KIND; +import static software.amazon.opentelemetry.javaagent.providers.MetricAttributeGenerator.DEPENDENCY_METRIC; +import static software.amazon.opentelemetry.javaagent.providers.MetricAttributeGenerator.SERVICE_METRIC; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +/** Unit tests for {@link AwsAttributeGeneratingSpanProcessor}. */ +class AwsAttributeGeneratingSpanProcessorTest { + + private static final Resource testResource = Resource.empty(); + + private Tracer tracer; + private MetricAttributeGenerator generatorMock; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + generatorMock = mock(MetricAttributeGenerator.class); + + tracer = + SdkTracerProvider.builder() + .addSpanProcessor( + AwsAttributeGeneratingSpanProcessor.create(generatorMock, testResource)) + .build() + .get("awsxray"); + } + + @Test + public void testOnEndingGeneratesBothWithoutOverride() { + Span span = tracer.spanBuilder("test").setSpanKind(SpanKind.CLIENT).startSpan(); + + // Mock attributes returned by generator + Attributes metricAttributes = + Attributes.of( + AWS_LOCAL_SERVICE, + "SERVICE", + AWS_SPAN_KIND, + SpanKind.CLIENT.name(), + AttributeKey.stringKey("key"), + "val"); + Map attributeMap = new HashMap<>(); + attributeMap.put(DEPENDENCY_METRIC, metricAttributes); + when(generatorMock.generateMetricAttributeMapFromSpan(any(SpanData.class), any(Resource.class))) + .thenReturn(attributeMap); + + // End span and verify it was modified appropriately + span.end(); + ReadableSpan readableSpan = (ReadableSpan) span; + assertThat(readableSpan.getAttribute(AWS_SPAN_KIND)) + .isEqualTo(AwsSpanProcessingUtil.LOCAL_ROOT); + assertThat(readableSpan.getAttribute(AWS_LOCAL_SERVICE)).isEqualTo("SERVICE"); + assertThat(readableSpan.getAttributes().size()).isEqualTo(metricAttributes.size()); + } + + @Test + public void testOnEndingGeneratesBothWithOverride() { + Span span = + tracer + .spanBuilder("test") + .setSpanKind(SpanKind.CLIENT) + .setAttribute("start_key", "start_val") + .setAttribute("was_changed", false) + .startSpan(); + + // Mock attributes returned by generator + Attributes metricAttributes = + Attributes.builder() + .put(AWS_LOCAL_SERVICE, "SERVICE") + .put(AWS_SPAN_KIND, SpanKind.CLIENT.name()) + .put("ending_key", "ending_val") + .put("was_changed", true) + .build(); + Map attributeMap = new HashMap<>(); + attributeMap.put(DEPENDENCY_METRIC, metricAttributes); + when(generatorMock.generateMetricAttributeMapFromSpan(any(SpanData.class), any(Resource.class))) + .thenReturn(attributeMap); + + // End span and verify it was modified appropriately + span.end(); + ReadableSpan readableSpan = (ReadableSpan) span; + assertThat(readableSpan.getAttribute(AWS_SPAN_KIND)) + .isEqualTo(AwsSpanProcessingUtil.LOCAL_ROOT); + assertThat(readableSpan.getAttribute(AWS_LOCAL_SERVICE)).isEqualTo("SERVICE"); + assertThat(readableSpan.getAttribute(AttributeKey.booleanKey("was_changed"))).isEqualTo(true); + // 2 start attributes + 4 ending attributes - 1 overlap + assertThat(readableSpan.getAttributes().size()).isEqualTo(5); + } + + @Test + public void testOnEndingGeneratesService() { + Span span = tracer.spanBuilder("test").setSpanKind(SpanKind.SERVER).startSpan(); + + // Mock attributes returned by generator + Attributes metricAttributes = + Attributes.of(AWS_LOCAL_SERVICE, "SERVICE", AWS_SPAN_KIND, SpanKind.SERVER.name()); + Map attributeMap = new HashMap<>(); + attributeMap.put(SERVICE_METRIC, metricAttributes); + when(generatorMock.generateMetricAttributeMapFromSpan(any(SpanData.class), any(Resource.class))) + .thenReturn(attributeMap); + + // End span and verify it was modified appropriately + span.end(); + ReadableSpan readableSpan = (ReadableSpan) span; + assertThat(readableSpan.getAttribute(AWS_LOCAL_SERVICE)).isEqualTo("SERVICE"); + assertThat(readableSpan.getAttribute(AWS_SPAN_KIND)).isEqualTo(SpanKind.SERVER.name()); + assertThat(readableSpan.getAttributes().size()).isEqualTo(2); + } + + @Test + public void testOnEndingGeneratesDependency() { + Span span = + tracer + .spanBuilder("test") + .setSpanKind(SpanKind.CONSUMER) + .setParent(mock(Context.class)) + .startSpan(); + + // Mock attributes returned by generator + Attributes metricAttributes = + Attributes.of(AWS_LOCAL_SERVICE, "SERVICE", AWS_SPAN_KIND, SpanKind.CONSUMER.name()); + Map attributeMap = new HashMap<>(); + attributeMap.put(DEPENDENCY_METRIC, metricAttributes); + when(generatorMock.generateMetricAttributeMapFromSpan(any(SpanData.class), any(Resource.class))) + .thenReturn(attributeMap); + + // Ensure span is treated as only having dependency metric information + try (MockedStatic mockUtil = + Mockito.mockStatic(AwsSpanProcessingUtil.class)) { + mockUtil + .when( + () -> + AwsSpanProcessingUtil.shouldGenerateServiceMetricAttributes(any(SpanData.class))) + .thenReturn(false); + mockUtil + .when( + () -> + AwsSpanProcessingUtil.shouldGenerateDependencyMetricAttributes( + any(SpanData.class))) + .thenReturn(true); + + // End span and verify it was modified appropriately + span.end(); + ReadableSpan readableSpan = (ReadableSpan) span; + assertThat(readableSpan.getAttribute(AWS_LOCAL_SERVICE)).isEqualTo("SERVICE"); + assertThat(readableSpan.getAttribute(AWS_SPAN_KIND)).isEqualTo(SpanKind.CONSUMER.name()); + assertThat(readableSpan.getAttributes().size()).isEqualTo(2); + } + } + + @Test + public void testOnEndingGeneratesBothWithTwoMetrics() { + Span span = tracer.spanBuilder("test").setSpanKind(SpanKind.CLIENT).startSpan(); + + // Mock attributes returned by generator + Map attributeMap = new HashMap<>(); + Attributes serviceMetricAttributes = + Attributes.of(AttributeKey.stringKey("new service key"), "new service value"); + attributeMap.put(SERVICE_METRIC, serviceMetricAttributes); + Attributes dependencyMetricAttributes = + Attributes.of( + AttributeKey.stringKey("new dependency key"), + "new dependency value", + AWS_SPAN_KIND, + SpanKind.PRODUCER.name()); + attributeMap.put(DEPENDENCY_METRIC, dependencyMetricAttributes); + when(generatorMock.generateMetricAttributeMapFromSpan(any(SpanData.class), any(Resource.class))) + .thenReturn(attributeMap); + + // End span and verify it was modified appropriately + span.end(); + ReadableSpan readableSpan = (ReadableSpan) span; + assertThat(readableSpan.getAttribute(AWS_SPAN_KIND)) + .isEqualTo(AwsSpanProcessingUtil.LOCAL_ROOT); + assertThat(readableSpan.getAttribute(AttributeKey.stringKey("new dependency key"))) + .isEqualTo("new dependency value"); + assertThat(readableSpan.getAttributes().size()).isEqualTo(dependencyMetricAttributes.size()); + } + + @Test + public void testConsumerSpanHasEmptyAttributes() { + AwsAttributeGeneratingSpanProcessor processor = + AwsAttributeGeneratingSpanProcessorBuilder.create(Resource.create(Attributes.empty())) + .build(); + + Attributes attributesMock = mock(Attributes.class); + SpanData spanDataMock = mock(SpanData.class); + SpanContext parentSpanContextMock = mock(SpanContext.class); + ReadWriteSpan readWriteSpanMock = mock(ReadWriteSpan.class); + + when(attributesMock.get(AwsAttributeKeys.AWS_CONSUMER_PARENT_SPAN_KIND)) + .thenReturn(SpanKind.CONSUMER.name()); + when(attributesMock.get(MESSAGING_OPERATION)).thenReturn(PROCESS); + when(spanDataMock.getKind()).thenReturn(SpanKind.CONSUMER); + when(spanDataMock.getAttributes()).thenReturn(attributesMock); + when(spanDataMock.getParentSpanContext()).thenReturn(parentSpanContextMock); + when(parentSpanContextMock.isValid()).thenReturn(true); + when(parentSpanContextMock.isRemote()).thenReturn(false); + when(readWriteSpanMock.toSpanData()).thenReturn(spanDataMock); + + // The dependencyAttributesMock will only be used if + // AwsSpanProcessingUtil.shouldGenerateDependencyMetricAttributes(span) is true. + // It shouldn't have any interaction since the spanData is a consumer process with parent span + // of consumer + Map attributeMap = new HashMap<>(); + Attributes dependencyAttributesMock = mock(Attributes.class); + attributeMap.put(DEPENDENCY_METRIC, dependencyAttributesMock); + // Configure generated attributes + when(generatorMock.generateMetricAttributeMapFromSpan(eq(spanDataMock), eq(testResource))) + .thenReturn(attributeMap); + + // End span and verify it was modified appropriately + processor.onEnding(readWriteSpanMock); + verify(readWriteSpanMock, times(0)).setAttribute(eq(AWS_SPAN_KIND), any()); + verify(readWriteSpanMock, times(0)).setAllAttributes(any()); + } +} diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributesSpanExporterTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributesSpanExporterTest.java deleted file mode 100644 index f2c0a9d01b..0000000000 --- a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributesSpanExporterTest.java +++ /dev/null @@ -1,487 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file 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 software.amazon.opentelemetry.javaagent.providers; - -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MessagingOperationTypeIncubatingValues.PROCESS; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; -import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_SPAN_KIND; -import static software.amazon.opentelemetry.javaagent.providers.MetricAttributeGenerator.DEPENDENCY_METRIC; -import static software.amazon.opentelemetry.javaagent.providers.MetricAttributeGenerator.SERVICE_METRIC; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.trace.data.EventData; -import io.opentelemetry.sdk.trace.data.LinkData; -import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.sdk.trace.export.SpanExporter; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.MockitoAnnotations; - -/** Unit tests for {@link AwsSpanMetricsProcessor}. */ -class AwsMetricAttributesSpanExporterTest { - - @Captor private static ArgumentCaptor> delegateExportCaptor; - - // Test constants - private static final boolean CONTAINS_ATTRIBUTES = true; - private static final boolean CONTAINS_NO_ATTRIBUTES = false; - - // Resource is not mockable, but tests can safely rely on an empty resource. - private static final Resource testResource = Resource.empty(); - - // Mocks required for tests. - private MetricAttributeGenerator generatorMock; - private SpanExporter delegateMock; - - private AwsMetricAttributesSpanExporter awsMetricAttributesSpanExporter; - - @BeforeEach - public void setUpMocks() { - MockitoAnnotations.openMocks(this); - generatorMock = mock(MetricAttributeGenerator.class); - delegateMock = mock(SpanExporter.class); - - awsMetricAttributesSpanExporter = - AwsMetricAttributesSpanExporter.create(delegateMock, generatorMock, testResource); - } - - @Test - public void testPassthroughDelegations() { - awsMetricAttributesSpanExporter.flush(); - awsMetricAttributesSpanExporter.shutdown(); - awsMetricAttributesSpanExporter.close(); - verify(delegateMock, times(1)).flush(); - verify(delegateMock, times(1)).shutdown(); - verify(delegateMock, times(1)).close(); - } - - @Test - public void testExportDelegationWithoutAttributeOrModification() { - Attributes spanAttributes = buildSpanAttributes(CONTAINS_NO_ATTRIBUTES); - SpanData spanDataMock = buildSpanDataMock(spanAttributes); - Attributes metricAttributes = buildMetricAttributes(CONTAINS_NO_ATTRIBUTES); - configureMocksForExport(spanDataMock, metricAttributes); - - awsMetricAttributesSpanExporter.export(Collections.singletonList(spanDataMock)); - verify(delegateMock, times(1)).export(delegateExportCaptor.capture()); - Collection exportedSpans = delegateExportCaptor.getValue(); - assertThat(exportedSpans.size()).isEqualTo(1); - - SpanData exportedSpan = (SpanData) exportedSpans.toArray()[0]; - assertThat(exportedSpan).isEqualTo(spanDataMock); - } - - @Test - public void testExportDelegationWithAttributeButWithoutModification() { - Attributes spanAttributes = buildSpanAttributes(CONTAINS_ATTRIBUTES); - SpanData spanDataMock = buildSpanDataMock(spanAttributes); - Attributes metricAttributes = buildMetricAttributes(CONTAINS_NO_ATTRIBUTES); - configureMocksForExport(spanDataMock, metricAttributes); - - awsMetricAttributesSpanExporter.export(Collections.singletonList(spanDataMock)); - verify(delegateMock, times(1)).export(delegateExportCaptor.capture()); - Collection exportedSpans = delegateExportCaptor.getValue(); - assertThat(exportedSpans.size()).isEqualTo(1); - - SpanData exportedSpan = (SpanData) exportedSpans.toArray()[0]; - assertThat(exportedSpan).isEqualTo(spanDataMock); - } - - @Test - public void testExportDelegationWithoutAttributeButWithModification() { - Attributes spanAttributes = buildSpanAttributes(CONTAINS_NO_ATTRIBUTES); - SpanData spanDataMock = buildSpanDataMock(spanAttributes); - Attributes metricAttributes = buildMetricAttributes(CONTAINS_ATTRIBUTES); - configureMocksForExport(spanDataMock, metricAttributes); - - awsMetricAttributesSpanExporter.export(Collections.singletonList(spanDataMock)); - verify(delegateMock, times(1)).export(delegateExportCaptor.capture()); - List exportedSpans = (List) delegateExportCaptor.getValue(); - assertThat(exportedSpans.size()).isEqualTo(1); - - SpanData exportedSpan = exportedSpans.get(0); - assertThat(exportedSpan.getClass()).isNotEqualTo(spanDataMock.getClass()); - assertThat(exportedSpan.getTotalAttributeCount()).isEqualTo(metricAttributes.size()); - Attributes exportedAttributes = exportedSpan.getAttributes(); - assertThat(exportedAttributes.size()).isEqualTo(metricAttributes.size()); - metricAttributes.forEach((k, v) -> assertThat(exportedAttributes.get(k)).isEqualTo(v)); - } - - @Test - public void testExportDelegationWithAttributeAndModification() { - Attributes spanAttributes = buildSpanAttributes(CONTAINS_ATTRIBUTES); - SpanData spanDataMock = buildSpanDataMock(spanAttributes); - Attributes metricAttributes = buildMetricAttributes(CONTAINS_ATTRIBUTES); - configureMocksForExport(spanDataMock, metricAttributes); - - awsMetricAttributesSpanExporter.export(Collections.singletonList(spanDataMock)); - verify(delegateMock, times(1)).export(delegateExportCaptor.capture()); - List exportedSpans = (List) delegateExportCaptor.getValue(); - assertThat(exportedSpans.size()).isEqualTo(1); - - SpanData exportedSpan = exportedSpans.get(0); - assertThat(exportedSpan.getClass()).isNotEqualTo(spanDataMock.getClass()); - int expectedAttributeCount = metricAttributes.size() + spanAttributes.size(); - assertThat(exportedSpan.getTotalAttributeCount()).isEqualTo(expectedAttributeCount); - Attributes exportedAttributes = exportedSpan.getAttributes(); - assertThat(exportedAttributes.size()).isEqualTo(expectedAttributeCount); - spanAttributes.forEach((k, v) -> assertThat(exportedAttributes.get(k)).isEqualTo(v)); - metricAttributes.forEach((k, v) -> assertThat(exportedAttributes.get(k)).isEqualTo(v)); - } - - @Test - public void testExportDelegationWithMultipleSpans() { - Attributes spanAttributes1 = buildSpanAttributes(CONTAINS_NO_ATTRIBUTES); - SpanData spanDataMock1 = buildSpanDataMock(spanAttributes1); - Attributes metricAttributes1 = buildMetricAttributes(CONTAINS_NO_ATTRIBUTES); - configureMocksForExport(spanDataMock1, metricAttributes1); - - Attributes spanAttributes2 = buildSpanAttributes(CONTAINS_ATTRIBUTES); - SpanData spanDataMock2 = buildSpanDataMock(spanAttributes2); - Attributes metricAttributes2 = buildMetricAttributes(CONTAINS_ATTRIBUTES); - configureMocksForExport(spanDataMock2, metricAttributes2); - - Attributes spanAttributes3 = buildSpanAttributes(CONTAINS_ATTRIBUTES); - SpanData spanDataMock3 = buildSpanDataMock(spanAttributes3); - Attributes metricAttributes3 = buildMetricAttributes(CONTAINS_NO_ATTRIBUTES); - configureMocksForExport(spanDataMock3, metricAttributes3); - - awsMetricAttributesSpanExporter.export( - Arrays.asList(spanDataMock1, spanDataMock2, spanDataMock3)); - verify(delegateMock, times(1)).export(delegateExportCaptor.capture()); - List exportedSpans = (List) delegateExportCaptor.getValue(); - assertThat(exportedSpans.size()).isEqualTo(3); - - SpanData exportedSpan1 = exportedSpans.get(0); - SpanData exportedSpan2 = exportedSpans.get(1); - SpanData exportedSpan3 = exportedSpans.get(2); - - assertThat(exportedSpan1).isEqualTo(spanDataMock1); - assertThat(exportedSpan3).isEqualTo(spanDataMock3); - - assertThat(exportedSpan2.getClass()).isNotEqualTo(spanDataMock2.getClass()); - int expectedAttributeCount = metricAttributes2.size() + spanAttributes2.size(); - assertThat(exportedSpan2.getTotalAttributeCount()).isEqualTo(expectedAttributeCount); - Attributes exportedAttributes = exportedSpan2.getAttributes(); - assertThat(exportedAttributes.size()).isEqualTo(expectedAttributeCount); - spanAttributes2.forEach((k, v) -> assertThat(exportedAttributes.get(k)).isEqualTo(v)); - metricAttributes2.forEach((k, v) -> assertThat(exportedAttributes.get(k)).isEqualTo(v)); - } - - @Test - public void testOverridenAttributes() { - Attributes spanAttributes = - Attributes.of( - AttributeKey.stringKey("key1"), - "old value1", - AttributeKey.stringKey("key2"), - "old value2"); - SpanData spanDataMock = buildSpanDataMock(spanAttributes); - Attributes metricAttributes = - Attributes.of( - AttributeKey.stringKey("key1"), - "new value1", - AttributeKey.stringKey("key3"), - "new value3"); - configureMocksForExport(spanDataMock, metricAttributes); - - awsMetricAttributesSpanExporter.export(Collections.singletonList(spanDataMock)); - verify(delegateMock, times(1)).export(delegateExportCaptor.capture()); - List exportedSpans = (List) delegateExportCaptor.getValue(); - assertThat(exportedSpans.size()).isEqualTo(1); - - SpanData exportedSpan = exportedSpans.get(0); - assertThat(exportedSpan.getClass()).isNotEqualTo(spanDataMock.getClass()); - assertThat(exportedSpan.getTotalAttributeCount()).isEqualTo(3); - Attributes exportedAttributes = exportedSpan.getAttributes(); - assertThat(exportedAttributes.size()).isEqualTo(3); - assertThat(exportedAttributes.get(AttributeKey.stringKey("key1"))).isEqualTo("new value1"); - assertThat(exportedAttributes.get(AttributeKey.stringKey("key2"))).isEqualTo("old value2"); - assertThat(exportedAttributes.get(AttributeKey.stringKey("key3"))).isEqualTo("new value3"); - } - - @Test - public void testExportDelegatingSpanDataBehaviour() { - Attributes spanAttributes = buildSpanAttributes(CONTAINS_ATTRIBUTES); - SpanData spanDataMock = buildSpanDataMock(spanAttributes); - Attributes metricAttributes = buildMetricAttributes(CONTAINS_ATTRIBUTES); - configureMocksForExport(spanDataMock, metricAttributes); - - awsMetricAttributesSpanExporter.export(Collections.singletonList(spanDataMock)); - verify(delegateMock, times(1)).export(delegateExportCaptor.capture()); - List exportedSpans = (List) delegateExportCaptor.getValue(); - assertThat(exportedSpans.size()).isEqualTo(1); - - SpanData exportedSpan = exportedSpans.get(0); - - SpanContext spanContextMock = mock(SpanContext.class); - when(spanDataMock.getSpanContext()).thenReturn(spanContextMock); - assertThat(exportedSpan.getSpanContext()).isEqualTo(spanContextMock); - - SpanContext parentSpanContextMock = mock(SpanContext.class); - when(spanDataMock.getParentSpanContext()).thenReturn(parentSpanContextMock); - assertThat(exportedSpan.getParentSpanContext()).isEqualTo(parentSpanContextMock); - - when(spanDataMock.getResource()).thenReturn(testResource); - assertThat(exportedSpan.getResource()).isEqualTo(testResource); - - // InstrumentationLibraryInfo is deprecated, so actually invoking it causes build failures. - // Excluding from this test. - - InstrumentationScopeInfo testInstrumentationScopeInfo = InstrumentationScopeInfo.empty(); - when(spanDataMock.getInstrumentationScopeInfo()).thenReturn(testInstrumentationScopeInfo); - assertThat(exportedSpan.getInstrumentationScopeInfo()).isEqualTo(testInstrumentationScopeInfo); - - String testName = "name"; - when(spanDataMock.getName()).thenReturn(testName); - assertThat(exportedSpan.getName()).isEqualTo(testName); - - SpanKind kindMock = mock(SpanKind.class); - when(spanDataMock.getKind()).thenReturn(kindMock); - assertThat(exportedSpan.getKind()).isEqualTo(kindMock); - - long testStartEpochNanos = 1L; - when(spanDataMock.getStartEpochNanos()).thenReturn(testStartEpochNanos); - assertThat(exportedSpan.getStartEpochNanos()).isEqualTo(testStartEpochNanos); - - List eventsMock = Collections.singletonList(mock(EventData.class)); - when(spanDataMock.getEvents()).thenReturn(eventsMock); - assertThat(exportedSpan.getEvents()).isEqualTo(eventsMock); - - List linksMock = Collections.singletonList(mock(LinkData.class)); - when(spanDataMock.getLinks()).thenReturn(linksMock); - assertThat(exportedSpan.getLinks()).isEqualTo(linksMock); - - StatusData statusMock = mock(StatusData.class); - when(spanDataMock.getStatus()).thenReturn(statusMock); - assertThat(exportedSpan.getStatus()).isEqualTo(statusMock); - - long testEndEpochNanosMock = 2L; - when(spanDataMock.getEndEpochNanos()).thenReturn(testEndEpochNanosMock); - assertThat(exportedSpan.getEndEpochNanos()).isEqualTo(testEndEpochNanosMock); - - when(spanDataMock.hasEnded()).thenReturn(true); - assertThat(exportedSpan.hasEnded()).isEqualTo(true); - - int testTotalRecordedEventsMock = 3; - when(spanDataMock.getTotalRecordedEvents()).thenReturn(testTotalRecordedEventsMock); - assertThat(exportedSpan.getTotalRecordedEvents()).isEqualTo(testTotalRecordedEventsMock); - - int testTotalRecordedLinksMock = 4; - when(spanDataMock.getTotalRecordedLinks()).thenReturn(testTotalRecordedLinksMock); - assertThat(exportedSpan.getTotalRecordedLinks()).isEqualTo(testTotalRecordedLinksMock); - } - - @Test - public void testExportDelegationWithTwoMetrics() { - // Original Span Attribute - Attributes spanAttributes = buildSpanAttributes(CONTAINS_ATTRIBUTES); - - // Create new span data mock - SpanData spanDataMock = mock(SpanData.class); - when(spanDataMock.getAttributes()).thenReturn(spanAttributes); - when(spanDataMock.getTotalAttributeCount()).thenReturn(spanAttributes.size()); - when(spanDataMock.getKind()).thenReturn(SpanKind.PRODUCER); - when(spanDataMock.getParentSpanContext()).thenReturn(null); - - // Create mock for the generateMetricAttributeMapFromSpan. Returns both dependency and service - // metric - Map attributeMap = new HashMap<>(); - Attributes serviceMtricAttributes = - Attributes.of(AttributeKey.stringKey("new service key"), "new service value"); - attributeMap.put(SERVICE_METRIC, serviceMtricAttributes); - - Attributes dependencyMetricAttributes = - Attributes.of( - AttributeKey.stringKey("new dependency key"), - "new dependency value", - AWS_SPAN_KIND, - SpanKind.PRODUCER.name()); - attributeMap.put(DEPENDENCY_METRIC, dependencyMetricAttributes); - - when(generatorMock.generateMetricAttributeMapFromSpan(eq(spanDataMock), eq(testResource))) - .thenReturn(attributeMap); - - awsMetricAttributesSpanExporter.export(Collections.singletonList(spanDataMock)); - verify(delegateMock, times(1)).export(delegateExportCaptor.capture()); - List exportedSpans = (List) delegateExportCaptor.getValue(); - assertThat(exportedSpans.size()).isEqualTo(1); - - // Retrieve the returned span - SpanData exportedSpan = exportedSpans.get(0); - - // Check the number of attributes - int expectedAttributeCount = dependencyMetricAttributes.size() + spanAttributes.size(); - assertThat(exportedSpan.getTotalAttributeCount()).isEqualTo(expectedAttributeCount); - Attributes exportedAttributes = exportedSpan.getAttributes(); - assertThat(exportedAttributes.size()).isEqualTo(expectedAttributeCount); - - // Check that all expected attributes are present - spanAttributes.forEach((k, v) -> assertThat(exportedAttributes.get(k)).isEqualTo(v)); - dependencyMetricAttributes.forEach( - (k, v) -> { - if (k.equals(AWS_SPAN_KIND)) { - assertThat(exportedAttributes.get(k)).isNotEqualTo(v); - } else { - assertThat(exportedAttributes.get(k)).isEqualTo(v); - } - }); - assertThat(exportedAttributes.get(AwsAttributeKeys.AWS_SPAN_KIND)) - .isEqualTo(AwsSpanProcessingUtil.LOCAL_ROOT); - } - - @Test - public void testConsumerProcessSpanHasEmptyAttribute() { - Attributes attributesMock = mock(Attributes.class); - SpanData spanDataMock = mock(SpanData.class); - SpanContext parentSpanContextMock = mock(SpanContext.class); - - when(attributesMock.get(AwsAttributeKeys.AWS_CONSUMER_PARENT_SPAN_KIND)) - .thenReturn(SpanKind.CONSUMER.name()); - when(attributesMock.get(MESSAGING_OPERATION)).thenReturn(PROCESS); - when(spanDataMock.getKind()).thenReturn(SpanKind.CONSUMER); - when(spanDataMock.getAttributes()).thenReturn(attributesMock); - when(spanDataMock.getParentSpanContext()).thenReturn(parentSpanContextMock); - when(parentSpanContextMock.isValid()).thenReturn(true); - when(parentSpanContextMock.isRemote()).thenReturn(false); - - // The dependencyAttributesMock will only be used if - // AwsSpanProcessingUtil.shouldGenerateDependencyMetricAttributes(span) is true. - // It shouldn't have any interaction since the spanData is a consumer process with parent span - // of consumer - Map attributeMap = new HashMap<>(); - Attributes dependencyAttributesMock = mock(Attributes.class); - attributeMap.put(DEPENDENCY_METRIC, dependencyAttributesMock); - // Configure generated attributes - when(generatorMock.generateMetricAttributeMapFromSpan(eq(spanDataMock), eq(testResource))) - .thenReturn(attributeMap); - - awsMetricAttributesSpanExporter.export(Collections.singletonList(spanDataMock)); - verify(delegateMock, times(1)).export(delegateExportCaptor.capture()); - Collection exportedSpans = delegateExportCaptor.getValue(); - assertThat(exportedSpans.size()).isEqualTo(1); - - verifyNoInteractions(dependencyAttributesMock); - - SpanData exportedSpan = (SpanData) exportedSpans.toArray()[0]; - assertThat(exportedSpan).isEqualTo(spanDataMock); - } - - @Test - public void testExportDelegationWithDependencyMetrics() { - // Original Span Attribute - Attributes spanAttributes = buildSpanAttributes(CONTAINS_ATTRIBUTES); - - // Create new span data mock - SpanData spanDataMock = mock(SpanData.class); - SpanContext spanContextMock = mock(SpanContext.class); - when(spanContextMock.isRemote()).thenReturn(false); - when(spanContextMock.isValid()).thenReturn(true); - when(spanDataMock.getAttributes()).thenReturn(spanAttributes); - when(spanDataMock.getTotalAttributeCount()).thenReturn(spanAttributes.size()); - when(spanDataMock.getKind()).thenReturn(SpanKind.PRODUCER); - when(spanDataMock.getParentSpanContext()).thenReturn(spanContextMock); - - // Create mock for the generateMetricAttributeMapFromSpan. Returns dependency metric - Map attributeMap = new HashMap<>(); - Attributes metricAttributes = - Attributes.of(AttributeKey.stringKey("new service key"), "new dependency value"); - attributeMap.put(DEPENDENCY_METRIC, metricAttributes); - - when(generatorMock.generateMetricAttributeMapFromSpan(eq(spanDataMock), eq(testResource))) - .thenReturn(attributeMap); - - awsMetricAttributesSpanExporter.export(Collections.singletonList(spanDataMock)); - verify(delegateMock, times(1)).export(delegateExportCaptor.capture()); - List exportedSpans = (List) delegateExportCaptor.getValue(); - assertThat(exportedSpans.size()).isEqualTo(1); - - // Retrieve the returned span - SpanData exportedSpan = exportedSpans.get(0); - - // Check the number of attributes - int expectedAttributeCount = metricAttributes.size() + spanAttributes.size(); - assertThat(exportedSpan.getTotalAttributeCount()).isEqualTo(expectedAttributeCount); - Attributes exportedAttributes = exportedSpan.getAttributes(); - assertThat(exportedAttributes.size()).isEqualTo(expectedAttributeCount); - - // Check that all expected attributes are present - spanAttributes.forEach((k, v) -> assertThat(exportedAttributes.get(k)).isEqualTo(v)); - metricAttributes.forEach((k, v) -> assertThat(exportedAttributes.get(k)).isEqualTo(v)); - } - - private static Attributes buildSpanAttributes(boolean containsAttribute) { - if (containsAttribute) { - return Attributes.of(AttributeKey.stringKey("original key"), "original value"); - } else { - return Attributes.empty(); - } - } - - private static Attributes buildMetricAttributes(boolean containsAttribute) { - if (containsAttribute) { - return Attributes.of(AttributeKey.stringKey("new key"), "new value"); - } else { - return Attributes.empty(); - } - } - - private static SpanData buildSpanDataMock(Attributes spanAttributes) { - // Configure spanData - SpanData mockSpanData = mock(SpanData.class); - when(mockSpanData.getAttributes()).thenReturn(spanAttributes); - when(mockSpanData.getTotalAttributeCount()).thenReturn(spanAttributes.size()); - when(mockSpanData.getKind()).thenReturn(SpanKind.SERVER); - when(mockSpanData.getParentSpanContext()).thenReturn(null); - return mockSpanData; - } - - private void configureMocksForExport(SpanData spanDataMock, Attributes metricAttributes) { - Map attributeMap = new HashMap<>(); - if (AwsSpanProcessingUtil.shouldGenerateServiceMetricAttributes(spanDataMock)) { - attributeMap.put(SERVICE_METRIC, metricAttributes); - } - - if (AwsSpanProcessingUtil.shouldGenerateDependencyMetricAttributes(spanDataMock)) { - attributeMap.put(DEPENDENCY_METRIC, metricAttributes); - } - - // Configure generated attributes - when(generatorMock.generateMetricAttributeMapFromSpan(eq(spanDataMock), eq(testResource))) - .thenReturn(attributeMap); - } -}