diff --git a/Dockerfile b/Dockerfile index e8aa35db1..23fc2c907 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,9 @@ # The packages are installed in the `/autoinstrumentation` directory. This is required as when instrumenting the pod by CWOperator, # one init container will be created to copy all the content in `/autoinstrumentation` directory to app's container. Then # update the `PYTHONPATH` environment variable accordingly. Then in the second stage, copy the directory to `/autoinstrumentation`. -FROM python:3.11 AS build + +# Stage 1: Install ADOT Python in the /operator-build folder +FROM public.ecr.aws/docker/library/python:3.11 AS build WORKDIR /operator-build @@ -18,11 +20,42 @@ RUN sed -i "/opentelemetry-exporter-otlp-proto-grpc/d" ./aws-opentelemetry-distr RUN mkdir workspace && pip install --target workspace ./aws-opentelemetry-distro -FROM public.ecr.aws/amazonlinux/amazonlinux:minimal +# Stage 2: Build the cp-utility binary +FROM public.ecr.aws/docker/library/rust:1.75 as builder + +WORKDIR /usr/src/cp-utility +COPY ./tools/cp-utility . + +## TARGETARCH is defined by buildx +# https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope +ARG TARGETARCH + +# Run validations and audit only on amd64 because it is faster and those two steps +# are only used to validate the source code and don't require anything that is +# architecture specific. + +# Validations +# Validate formatting +RUN if [ $TARGETARCH = "amd64" ]; then rustup component add rustfmt && cargo fmt --check ; fi + +# Audit dependencies +RUN if [ $TARGETARCH = "amd64" ]; then cargo install cargo-audit && cargo audit ; fi + + +# Cross-compile based on the target platform. +RUN if [ $TARGETARCH = "amd64" ]; then export ARCH="x86_64" ; \ + elif [ $TARGETARCH = "arm64" ]; then export ARCH="aarch64" ; \ + else false; \ + fi \ + && rustup target add ${ARCH}-unknown-linux-musl \ + && cargo test --target ${ARCH}-unknown-linux-musl \ + && cargo install --target ${ARCH}-unknown-linux-musl --path . --root . + +# Stage 3: Build the distribution image by copying the THIRD-PARTY-LICENSES, the custom built cp command from stage 2, and the installed ADOT Python from stage 1 to their respective destinations +FROM scratch # Required to copy attribute files to distributed docker images ADD THIRD-PARTY-LICENSES ./THIRD-PARTY-LICENSES +COPY --from=builder /usr/src/cp-utility/bin/cp-utility /bin/cp COPY --from=build /operator-build/workspace /autoinstrumentation - -RUN chmod -R go+r /autoinstrumentation diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_attribute_keys.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_attribute_keys.py index f6498ac76..197a566e8 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_attribute_keys.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_attribute_keys.py @@ -16,3 +16,5 @@ AWS_QUEUE_URL: str = "aws.sqs.queue_url" AWS_QUEUE_NAME: str = "aws.sqs.queue_name" AWS_STREAM_NAME: str = "aws.kinesis.stream_name" +AWS_LAMBDA_FUNCTION_NAME: str = "aws.lambda.function_name" +AWS_LAMBDA_SOURCE_MAPPING_ID: str = "aws.lambda.resource_mapping_id" diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py index 577d28f63..468c6d109 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py @@ -6,6 +6,8 @@ from urllib.parse import ParseResult, urlparse from amazon.opentelemetry.distro._aws_attribute_keys import ( + AWS_LAMBDA_FUNCTION_NAME, + AWS_LAMBDA_SOURCE_MAPPING_ID, AWS_LOCAL_OPERATION, AWS_LOCAL_SERVICE, AWS_QUEUE_NAME, @@ -78,6 +80,7 @@ _NORMALIZED_KINESIS_SERVICE_NAME: str = "AWS::Kinesis" _NORMALIZED_S3_SERVICE_NAME: str = "AWS::S3" _NORMALIZED_SQS_SERVICE_NAME: str = "AWS::SQS" +_NORMALIZED_LAMBDA_SERVICE_NAME: str = "AWS::Lambda" _DB_CONNECTION_STRING_TYPE: str = "DB::Connection" # Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present. @@ -372,6 +375,12 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri remote_resource_identifier = _escape_delimiters( SqsUrlParser.get_queue_name(span.attributes.get(AWS_QUEUE_URL)) ) + elif is_key_present(span, AWS_LAMBDA_SOURCE_MAPPING_ID): + remote_resource_type = _NORMALIZED_LAMBDA_SERVICE_NAME + "::EventSourceMapping" + remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_LAMBDA_SOURCE_MAPPING_ID)) + elif is_key_present(span, AWS_LAMBDA_FUNCTION_NAME): + remote_resource_type = _NORMALIZED_LAMBDA_SERVICE_NAME + "::Function" + remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_LAMBDA_FUNCTION_NAME)) elif is_db_span(span): remote_resource_type = _DB_CONNECTION_STRING_TYPE remote_resource_identifier = _get_db_connection(span) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py index cf73fb345..1e7e6b65f 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py @@ -4,6 +4,7 @@ import importlib from opentelemetry.instrumentation.botocore.extensions import _KNOWN_EXTENSIONS +from opentelemetry.instrumentation.botocore.extensions.lmbd import _LambdaExtension from opentelemetry.instrumentation.botocore.extensions.sqs import _SqsExtension from opentelemetry.instrumentation.botocore.extensions.types import _AttributeMapT, _AwsSdkExtension from opentelemetry.semconv.trace import SpanAttributes @@ -12,11 +13,12 @@ def _apply_botocore_instrumentation_patches() -> None: """Botocore instrumentation patches - Adds patches to provide additional support and Java parity for Kinesis, S3, and SQS. + Adds patches to provide additional support and Java parity for Kinesis, S3, SQS and Lambda. """ _apply_botocore_kinesis_patch() _apply_botocore_s3_patch() _apply_botocore_sqs_patch() + _apply_botocore_lambda_patch() def _apply_botocore_kinesis_patch() -> None: @@ -65,6 +67,29 @@ def patch_extract_attributes(self, attributes: _AttributeMapT): _SqsExtension.extract_attributes = patch_extract_attributes +def _apply_botocore_lambda_patch() -> None: + """Botocore instrumentation patch for Lambda + + This patch extends the existing upstream extension for Lambda. Extensions allow for custom logic for adding + service-specific information to spans, such as attributes. Specifically, we are adding logic to add + `aws.lambda.function_name` and `aws.lambda.resource_mapping_id` attributes. + Callout that today, the upstream logic adds `SpanAttributes.FAAS_INVOKED_NAME` for Invoke operation, + but we want to cover more operations to extract `FunctionName`, we define "aws.lambda.function_name" separately. + """ + old_extract_attributes = _LambdaExtension.extract_attributes + + def patch_extract_attributes(self, attributes: _AttributeMapT): + old_extract_attributes(self, attributes) + function_name = self._call_context.params.get("FunctionName") + resource_mapping_id = self._call_context.params.get("UUID") + if function_name: + attributes["aws.lambda.function_name"] = function_name + if resource_mapping_id: + attributes["aws.lambda.resource_mapping_id"] = resource_mapping_id + + _LambdaExtension.extract_attributes = patch_extract_attributes + + # The OpenTelemetry Authors code def _lazy_load(module, cls): """Clone of upstream opentelemetry.instrumentation.botocore.extensions.lazy_load diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py index 072e6eeb0..71e828c43 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py @@ -9,6 +9,8 @@ from amazon.opentelemetry.distro._aws_attribute_keys import ( AWS_CONSUMER_PARENT_SPAN_KIND, + AWS_LAMBDA_FUNCTION_NAME, + AWS_LAMBDA_SOURCE_MAPPING_ID, AWS_LOCAL_OPERATION, AWS_LOCAL_SERVICE, AWS_QUEUE_NAME, @@ -821,6 +823,7 @@ def test_normalize_remote_service_name_aws_sdk(self): self.validate_aws_sdk_service_normalization("Kinesis", "AWS::Kinesis") self.validate_aws_sdk_service_normalization("S3", "AWS::S3") self.validate_aws_sdk_service_normalization("SQS", "AWS::SQS") + self.validate_aws_sdk_service_normalization("Lambda", "AWS::Lambda") def validate_aws_sdk_service_normalization(self, service_name: str, expected_remote_service: str): self._mock_attribute([SpanAttributes.RPC_SYSTEM, SpanAttributes.RPC_SERVICE], ["aws-api", service_name]) @@ -977,6 +980,29 @@ def test_sdk_client_span_with_remote_resource_attributes(self): self._validate_remote_resource_attributes("AWS::DynamoDB::Table", "aws_table^^name") self._mock_attribute([SpanAttributes.AWS_DYNAMODB_TABLE_NAMES], [None]) + # Validate behaviour of AWS_LAMBDA_FUNCTION_NAME attribute, then remove it. + self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME], ["aws_lambda_function_name"], keys, values) + self._validate_remote_resource_attributes("AWS::Lambda::Function", "aws_lambda_function_name") + self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME], [None]) + + # Validate behaviour of AWS_LAMBDA_SOURCE_MAPPING_ID attribute, then remove it. + self._mock_attribute([AWS_LAMBDA_SOURCE_MAPPING_ID], ["aws_event_source_mapping_id"], keys, values) + self._validate_remote_resource_attributes("AWS::Lambda::EventSourceMapping", "aws_event_source_mapping_id") + self._mock_attribute([AWS_LAMBDA_SOURCE_MAPPING_ID], [None]) + + # Validate behaviour of AWS_LAMBDA_FUNCTION_NAME and AWS_LAMBDA_SOURCE_MAPPING_ID attributes both exist, + # then remove them. + self._mock_attribute( + [AWS_LAMBDA_FUNCTION_NAME, AWS_LAMBDA_SOURCE_MAPPING_ID], + ["aws_lambda_function_name", "aws_event_source_mapping_id"], + keys, + values, + ) + self._validate_remote_resource_attributes("AWS::Lambda::EventSourceMapping", "aws_event_source_mapping_id") + self._mock_attribute([AWS_LAMBDA_FUNCTION_NAME, AWS_LAMBDA_SOURCE_MAPPING_ID], [None, None]) + + # AWS_LAMBDA_SOURCE_MAPPING_ID + self._mock_attribute([SpanAttributes.RPC_SYSTEM], [None]) def test_client_db_span_with_remote_resource_attributes(self): diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py index bc6e851a9..ca5098e4a 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py @@ -14,6 +14,8 @@ _BUCKET_NAME: str = "bucketName" _QUEUE_NAME: str = "queueName" _QUEUE_URL: str = "queueUrl" +_LAMBDA_FUNCTION_NAME: str = "lambdaFunctionName" +_LAMBDA_SOURCE_MAPPING_ID: str = "lambdaEventSourceMappingID" class TestInstrumentationPatch(TestCase): @@ -74,6 +76,9 @@ def _validate_unpatched_botocore_instrumentation(self): self.assertFalse("aws.sqs.queue_url" in attributes) self.assertFalse("aws.sqs.queue_name" in attributes) + # Lambda + self.assertTrue("lambda" in _KNOWN_EXTENSIONS, "Upstream has removed the Lambda extension") + def _validate_patched_botocore_instrumentation(self): # Kinesis self.assertTrue("kinesis" in _KNOWN_EXTENSIONS) @@ -96,6 +101,14 @@ def _validate_patched_botocore_instrumentation(self): self.assertTrue("aws.sqs.queue_name" in sqs_attributes) self.assertEqual(sqs_attributes["aws.sqs.queue_name"], _QUEUE_NAME) + # Lambda + self.assertTrue("lambda" in _KNOWN_EXTENSIONS) + lambda_attributes: Dict[str, str] = _do_extract_lambda_attributes() + self.assertTrue("aws.lambda.function_name" in lambda_attributes) + self.assertEqual(lambda_attributes["aws.lambda.function_name"], _LAMBDA_FUNCTION_NAME) + self.assertTrue("aws.lambda.resource_mapping_id" in lambda_attributes) + self.assertEqual(lambda_attributes["aws.lambda.resource_mapping_id"], _LAMBDA_SOURCE_MAPPING_ID) + def _do_extract_kinesis_attributes() -> Dict[str, str]: service_name: str = "kinesis" @@ -115,6 +128,12 @@ def _do_extract_sqs_attributes() -> Dict[str, str]: return _do_extract_attributes(service_name, params) +def _do_extract_lambda_attributes() -> Dict[str, str]: + service_name: str = "lambda" + params: Dict[str, str] = {"FunctionName": _LAMBDA_FUNCTION_NAME, "UUID": _LAMBDA_SOURCE_MAPPING_ID} + return _do_extract_attributes(service_name, params) + + def _do_extract_attributes(service_name: str, params: Dict[str, str]) -> Dict[str, str]: mock_call_context: MagicMock = MagicMock() mock_call_context.params = params