diff --git a/.github/workflows/main-build.yml b/.github/workflows/main-build.yml index a268380681..2255a97aca 100644 --- a/.github/workflows/main-build.yml +++ b/.github/workflows/main-build.yml @@ -256,7 +256,7 @@ jobs: aws s3 cp ./build/distributions/aws-opentelemetry-java-layer.zip s3://adot-main-build-staging-jar/adot-java-lambda-layer-${{ github.run_id }}.zip application-signals-e2e-test: - needs: [build] + needs: [build, application-signals-lambda-layer-build] uses: ./.github/workflows/application-signals-e2e-test.yml secrets: inherit with: diff --git a/.github/workflows/release-lambda.yml b/.github/workflows/release-lambda.yml index 3257392a64..63ae2432d6 100644 --- a/.github/workflows/release-lambda.yml +++ b/.github/workflows/release-lambda.yml @@ -110,7 +110,7 @@ jobs: aws lambda publish-layer-version \ --layer-name ${{ env.LAYER_NAME }} \ --content S3Bucket=${{ env.BUCKET_NAME }},S3Key=aws-opentelemetry-java-layer.zip \ - --compatible-runtimes java17 java21 \ + --compatible-runtimes java11 java17 java21 \ --compatible-architectures "arm64" "x86_64" \ --license-info "Apache-2.0" \ --description "AWS Distro of OpenTelemetry Lambda Layer for Java Runtime" \ 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 d6bd420475..8cdd55a881 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 @@ -283,6 +283,10 @@ private Sampler customizeSampler(Sampler sampler, ConfigProperties configProps) private SdkTracerProviderBuilder customizeTracerProviderBuilder( SdkTracerProviderBuilder tracerProviderBuilder, ConfigProperties configProps) { + if (isLambdaEnvironment()) { + tracerProviderBuilder.addSpanProcessor(new AwsLambdaSpanProcessor()); + } + if (isApplicationSignalsEnabled(configProps)) { logger.info("AWS Application Signals enabled"); Duration exportInterval = @@ -294,9 +298,27 @@ private SdkTracerProviderBuilder customizeTracerProviderBuilder( // If running on Lambda, we just need to export 100% spans and skip generating any Application // Signals metrics. - if (isLambdaEnvironment()) { + if (isLambdaEnvironment() + && System.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_CONFIG) == null) { + String tracesEndpoint = + Optional.ofNullable(System.getenv(AWS_XRAY_DAEMON_ADDRESS_CONFIG)) + .orElse(DEFAULT_UDP_ENDPOINT); + SpanExporter spanExporter = + new OtlpUdpSpanExporterBuilder() + .setPayloadSampleDecision(TracePayloadSampleDecision.UNSAMPLED) + .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) .setMaxExportBatchSize(LAMBDA_SPAN_EXPORT_BATCH_SIZE) .build()); return tracerProviderBuilder; diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java index c9f3171c50..bd3a6ed785 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java @@ -110,4 +110,7 @@ private AwsAttributeKeys() {} AttributeKey.stringKey("aws.bedrock.guardrail.id"); static final AttributeKey AWS_GUARDRAIL_ARN = AttributeKey.stringKey("aws.bedrock.guardrail.arn"); + + static final AttributeKey AWS_TRACE_LAMBDA_MULTIPLE_SERVER = + AttributeKey.booleanKey("aws.trace.lambda.multiple-server"); } diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsLambdaSpanProcessor.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsLambdaSpanProcessor.java new file mode 100644 index 0000000000..65b5f6c07c --- /dev/null +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsLambdaSpanProcessor.java @@ -0,0 +1,55 @@ +/* + * 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 io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import javax.annotation.concurrent.Immutable; + +@Immutable +public final class AwsLambdaSpanProcessor implements SpanProcessor { + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + if (AwsSpanProcessingUtil.isServletServerSpan(span)) { + Span parentSpan = Span.fromContextOrNull(parentContext); + if (parentSpan == null || !(parentSpan instanceof ReadWriteSpan)) { + return; + } + + ReadWriteSpan parentReadWriteSpan = (ReadWriteSpan) parentSpan; + if (!AwsSpanProcessingUtil.isLambdaServerSpan(parentReadWriteSpan)) { + return; + } + parentReadWriteSpan.setAttribute(AwsAttributeKeys.AWS_TRACE_LAMBDA_MULTIPLE_SERVER, true); + } + } + + @Override + public boolean isStartRequired() { + 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/AwsSpanProcessingUtil.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtil.java index 4211c24a7c..b65d9526ef 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtil.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtil.java @@ -38,6 +38,7 @@ import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.data.SpanData; import java.io.IOException; import java.io.InputStream; @@ -69,6 +70,10 @@ final class AwsSpanProcessingUtil { private static final String SQL_DIALECT_KEYWORDS_JSON = "configuration/sql_dialect_keywords.json"; + static final AttributeKey OTEL_SCOPE_NAME = AttributeKey.stringKey("otel.scope.name"); + static final String LAMBDA_SCOPE_PREFIX = "io.opentelemetry.aws-lambda-"; + static final String SERVLET_SCOPE_PREFIX = "io.opentelemetry.servlet-"; + static List getDialectKeywords() { try (InputStream jsonFile = AwsSpanProcessingUtil.class @@ -108,6 +113,10 @@ static String getIngressOperation(SpanData span) { if (operationOverride != null) { return operationOverride; } + String op = generateIngressOperation(span); + if (!op.equals(UNKNOWN_OPERATION)) { + return op; + } return getFunctionNameFromEnv() + "/FunctionHandler"; } String operation = span.getName(); @@ -270,4 +279,30 @@ static boolean isDBSpan(SpanData span) { || isKeyPresent(span, DB_OPERATION) || isKeyPresent(span, DB_STATEMENT); } + + static boolean isLambdaServerSpan(ReadableSpan span) { + String scopeName = null; + if (span != null + && span.toSpanData() != null + && span.toSpanData().getInstrumentationScopeInfo() != null) { + scopeName = span.toSpanData().getInstrumentationScopeInfo().getName(); + } + + return scopeName != null + && scopeName.startsWith(LAMBDA_SCOPE_PREFIX) + && SpanKind.SERVER == span.getKind(); + } + + static boolean isServletServerSpan(ReadableSpan span) { + String scopeName = null; + if (span != null + && span.toSpanData() != null + && span.toSpanData().getInstrumentationScopeInfo() != null) { + scopeName = span.toSpanData().getInstrumentationScopeInfo().getName(); + } + + return scopeName != null + && scopeName.startsWith(SERVLET_SCOPE_PREFIX) + && SpanKind.SERVER == span.getKind(); + } } diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsLambdaSpanProcessorTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsLambdaSpanProcessorTest.java new file mode 100644 index 0000000000..19f1ae005c --- /dev/null +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsLambdaSpanProcessorTest.java @@ -0,0 +1,140 @@ +/* + * 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 org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.*; + +import io.opentelemetry.api.common.AttributeKey; +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.common.InstrumentationScopeInfo; +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.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AwsLambdaSpanProcessorTest { + + private AwsLambdaSpanProcessor processor; + private ReadWriteSpan mockLambdaServerSpan; + private SpanData mockLambdaSpanData; + private InstrumentationScopeInfo mockLambdaScopeInfo; + private Map, Object> attributeMapForLambdaSpan; + private SpanContext mockSpanContext; + + private ReadWriteSpan mockServletServerSpan; + private SpanData mockServletSpanData; + private InstrumentationScopeInfo mockServletScopeInfo; + + private Tracer lambdaTracer; + private Tracer servletTracer; + private Tracer otherTracer; + + @BeforeEach + public void setup() { + processor = new AwsLambdaSpanProcessor(); + lambdaTracer = + SdkTracerProvider.builder() + .addSpanProcessor(processor) + .build() + .get(AwsSpanProcessingUtil.LAMBDA_SCOPE_PREFIX + "core-1.0"); + + servletTracer = + SdkTracerProvider.builder() + .addSpanProcessor(processor) + .build() + .get(AwsSpanProcessingUtil.SERVLET_SCOPE_PREFIX + "lib-3.0"); + + otherTracer = + SdkTracerProvider.builder().addSpanProcessor(processor).build().get("other-lib-2.0"); + } + + @Test + void testOnStart_servletServerSpan_withLambdaServerSpan() { + Span parentSpan = + lambdaTracer.spanBuilder("parent-lambda").setSpanKind(SpanKind.SERVER).startSpan(); + servletTracer + .spanBuilder("child-servlet") + .setSpanKind(SpanKind.SERVER) + .setParent(Context.current().with(parentSpan)) + .startSpan(); + + ReadableSpan parentReadableSpan = (ReadableSpan) parentSpan; + assertThat(parentReadableSpan.getAttribute(AwsAttributeKeys.AWS_TRACE_LAMBDA_MULTIPLE_SERVER)) + .isEqualTo(true); + } + + @Test + void testOnStart_servletInternalSpan_withLambdaServerSpan() { + Span parentSpan = + lambdaTracer.spanBuilder("parent-lambda").setSpanKind(SpanKind.SERVER).startSpan(); + + servletTracer + .spanBuilder("child-servlet") + .setSpanKind(SpanKind.INTERNAL) + .setParent(Context.current().with(parentSpan)) + .startSpan(); + + ReadableSpan parentReadableSpan = (ReadableSpan) parentSpan; + assertNull(parentReadableSpan.getAttribute(AwsAttributeKeys.AWS_TRACE_LAMBDA_MULTIPLE_SERVER)); + } + + @Test + void testOnStart_servletServerSpan_withLambdaInternalSpan() { + Span parentSpan = + lambdaTracer.spanBuilder("parent-lambda").setSpanKind(SpanKind.INTERNAL).startSpan(); + + servletTracer + .spanBuilder("child-servlet") + .setSpanKind(SpanKind.SERVER) + .setParent(Context.current().with(parentSpan)) + .startSpan(); + + ReadableSpan parentReadableSpan = (ReadableSpan) parentSpan; + assertNull(parentReadableSpan.getAttribute(AwsAttributeKeys.AWS_TRACE_LAMBDA_MULTIPLE_SERVER)); + } + + @Test + void testOnStart_servletServerSpan_withLambdaServerSpanAsGrandParent() { + Span grandParentSpan = + lambdaTracer.spanBuilder("grandparent-lambda").setSpanKind(SpanKind.SERVER).startSpan(); + + Span parentSpan = + otherTracer + .spanBuilder("parent-other") + .setSpanKind(SpanKind.SERVER) + .setParent(Context.current().with(grandParentSpan)) + .startSpan(); + + servletTracer + .spanBuilder("child-servlet") + .setSpanKind(SpanKind.SERVER) + .setParent(Context.current().with(parentSpan)) + .startSpan(); + + ReadableSpan grandParentReadableSpan = (ReadableSpan) grandParentSpan; + assertNull( + grandParentReadableSpan.getAttribute(AwsAttributeKeys.AWS_TRACE_LAMBDA_MULTIPLE_SERVER)); + } +} diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtilTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtilTest.java index 9318a4c4ca..ea576a7303 100644 --- a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtilTest.java +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtilTest.java @@ -19,6 +19,8 @@ import static io.opentelemetry.semconv.SemanticAttributes.MessagingOperationValues.PROCESS; import static io.opentelemetry.semconv.SemanticAttributes.MessagingOperationValues.RECEIVE; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Answers.CALLS_REAL_METHODS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; @@ -33,6 +35,7 @@ import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.data.SpanData; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -451,4 +454,105 @@ public void testSqlDialectKeywordsMaxLength() { assertThat(MAX_KEYWORD_LENGTH >= keyword.length()); } } + + @Test + public void testIsLambdaServerSpan_withLambdaScope() { + ReadableSpan span = mock(ReadableSpan.class); + SpanData spanData = mock(SpanData.class); + InstrumentationScopeInfo scopeInfo = mock(InstrumentationScopeInfo.class); + when(span.toSpanData()).thenReturn(spanData); + when(spanData.getInstrumentationScopeInfo()).thenReturn(scopeInfo); + when(scopeInfo.getName()).thenReturn(AwsSpanProcessingUtil.LAMBDA_SCOPE_PREFIX + "-lib-1.0"); + when(span.getKind()).thenReturn(SpanKind.SERVER); + + assertTrue(AwsSpanProcessingUtil.isLambdaServerSpan(span)); + } + + @Test + public void testIsLambdaServerSpan_withNonLambdaScope() { + ReadableSpan span = mock(ReadableSpan.class); + SpanData spanData = mock(SpanData.class); + InstrumentationScopeInfo scopeInfo = mock(InstrumentationScopeInfo.class); + when(span.toSpanData()).thenReturn(spanData); + when(spanData.getInstrumentationScopeInfo()).thenReturn(scopeInfo); + when(scopeInfo.getName()) + .thenReturn("org.abc." + AwsSpanProcessingUtil.LAMBDA_SCOPE_PREFIX + "-lib-3.0"); + when(span.getKind()).thenReturn(SpanKind.SERVER); + + assertFalse(AwsSpanProcessingUtil.isLambdaServerSpan(span)); + } + + @Test + public void testIsLambdaServerSpan_withNullScope() { + ReadableSpan span = mock(ReadableSpan.class); + SpanData spanData = mock(SpanData.class); + when(span.toSpanData()).thenReturn(spanData); + when(spanData.getInstrumentationScopeInfo()).thenReturn(null); + when(span.getKind()).thenReturn(SpanKind.SERVER); + + assertFalse(AwsSpanProcessingUtil.isLambdaServerSpan(span)); + } + + @Test + public void testIsLambdaServerSpan_withNonServerSpanKind() { + ReadableSpan span = mock(ReadableSpan.class); + SpanData spanData = mock(SpanData.class); + InstrumentationScopeInfo scopeInfo = mock(InstrumentationScopeInfo.class); + when(span.toSpanData()).thenReturn(spanData); + when(spanData.getInstrumentationScopeInfo()).thenReturn(scopeInfo); + when(scopeInfo.getName()).thenReturn(AwsSpanProcessingUtil.LAMBDA_SCOPE_PREFIX + "-core-1.0"); + when(span.getKind()).thenReturn(SpanKind.CLIENT); + + assertFalse(AwsSpanProcessingUtil.isLambdaServerSpan(span)); + } + + @Test + public void testIsServletServerSpan_withServletScope() { + ReadableSpan span = mock(ReadableSpan.class); + SpanData spanData = mock(SpanData.class); + InstrumentationScopeInfo scopeInfo = mock(InstrumentationScopeInfo.class); + when(span.toSpanData()).thenReturn(spanData); + when(spanData.getInstrumentationScopeInfo()).thenReturn(scopeInfo); + when(scopeInfo.getName()).thenReturn(AwsSpanProcessingUtil.SERVLET_SCOPE_PREFIX + "-3.0"); + when(span.getKind()).thenReturn(SpanKind.SERVER); + + assertTrue(AwsSpanProcessingUtil.isServletServerSpan(span)); + } + + @Test + public void testIsServletServerSpan_withNonServletScope() { + ReadableSpan span = mock(ReadableSpan.class); + SpanData spanData = mock(SpanData.class); + InstrumentationScopeInfo scopeInfo = mock(InstrumentationScopeInfo.class); + when(span.toSpanData()).thenReturn(spanData); + when(spanData.getInstrumentationScopeInfo()).thenReturn(scopeInfo); + when(scopeInfo.getName()).thenReturn(AwsSpanProcessingUtil.LAMBDA_SCOPE_PREFIX + "-2.0"); + when(span.getKind()).thenReturn(SpanKind.SERVER); + + assertFalse(AwsSpanProcessingUtil.isServletServerSpan(span)); + } + + @Test + public void testIsServletServerSpan_withNullScope() { + ReadableSpan span = mock(ReadableSpan.class); + SpanData spanData = mock(SpanData.class); + when(span.toSpanData()).thenReturn(spanData); + when(spanData.getInstrumentationScopeInfo()).thenReturn(null); + when(span.getKind()).thenReturn(SpanKind.SERVER); + + assertFalse(AwsSpanProcessingUtil.isServletServerSpan(span)); + } + + @Test + public void testIsServletServerSpan_withNonServerSpanKind() { + ReadableSpan span = mock(ReadableSpan.class); + SpanData spanData = mock(SpanData.class); + InstrumentationScopeInfo scopeInfo = mock(InstrumentationScopeInfo.class); + when(span.toSpanData()).thenReturn(spanData); + when(spanData.getInstrumentationScopeInfo()).thenReturn(scopeInfo); + when(scopeInfo.getName()).thenReturn(AwsSpanProcessingUtil.SERVLET_SCOPE_PREFIX + "-5.0"); + when(span.getKind()).thenReturn(SpanKind.CLIENT); + + assertFalse(AwsSpanProcessingUtil.isServletServerSpan(span)); + } } diff --git a/lambda-layer/.gitignore b/lambda-layer/.gitignore index 1b6985c009..719d3e0657 100644 --- a/lambda-layer/.gitignore +++ b/lambda-layer/.gitignore @@ -3,3 +3,9 @@ # Ignore Gradle build output directory build + +# Ignore Terraform state files +.terraform/ +*.tfstate +*.tfstate.backup +*.lock.hcl \ No newline at end of file diff --git a/lambda-layer/build-layer.sh b/lambda-layer/build-layer.sh index 36350cd5b1..9b2c9b843a 100755 --- a/lambda-layer/build-layer.sh +++ b/lambda-layer/build-layer.sh @@ -76,4 +76,4 @@ popd ## Cleanup # revert the patch applied since it is only needed while building the layer. echo "Info: Cleanup" -git restore ../dependencyManagement/build.gradle.kts \ No newline at end of file +git restore ../dependencyManagement/build.gradle.kts diff --git a/lambda-layer/otel-instrument b/lambda-layer/otel-instrument index 07815ea51b..8bf5cf4657 100644 --- a/lambda-layer/otel-instrument +++ b/lambda-layer/otel-instrument @@ -65,4 +65,4 @@ fi ARGS=("${ARGS[0]}" "${EXTRA_ARGS[@]}" "${ARGS[@]:1}") -exec "${ARGS[@]}" \ No newline at end of file +exec "${ARGS[@]}"