Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Lambda support. #211

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 37 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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"
Expand All @@ -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
Expand Down