From 5e8e93d2efd18930b2de4b7c3377d5a32a4b476e Mon Sep 17 00:00:00 2001 From: Timothy Pansino <11214426+TimPansino@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:51:52 -0700 Subject: [PATCH] Add python's newrelic_lambda package directly to layers. (#225) Co-authored-by: Uma Annamalai --- python/newrelic_lambda/__init__.py | 1 + python/newrelic_lambda/agent_protocol.py | 74 ++++++ python/newrelic_lambda/event-sources.json | 128 ++++++++++ python/newrelic_lambda/lambda_handler.py | 274 ++++++++++++++++++++++ python/publish-layers.sh | 33 ++- 5 files changed, 499 insertions(+), 11 deletions(-) create mode 100644 python/newrelic_lambda/__init__.py create mode 100644 python/newrelic_lambda/agent_protocol.py create mode 100644 python/newrelic_lambda/event-sources.json create mode 100644 python/newrelic_lambda/lambda_handler.py diff --git a/python/newrelic_lambda/__init__.py b/python/newrelic_lambda/__init__.py new file mode 100644 index 00000000..2fa9836a --- /dev/null +++ b/python/newrelic_lambda/__init__.py @@ -0,0 +1 @@ +import newrelic_lambda.agent_protocol # noqa diff --git a/python/newrelic_lambda/agent_protocol.py b/python/newrelic_lambda/agent_protocol.py new file mode 100644 index 00000000..ac715e9b --- /dev/null +++ b/python/newrelic_lambda/agent_protocol.py @@ -0,0 +1,74 @@ +import logging +import os + +from newrelic.common.encoding_utils import ( + json_encode, + serverless_payload_encode, +) + +try: + from newrelic.core.agent_protocol import ServerlessModeProtocol +except ImportError: + ServerlessModeProtocol = None + +from newrelic.core.data_collector import ServerlessModeSession + +NAMED_PIPE_PATH = "/tmp/newrelic-telemetry" + +logger = logging.getLogger(__name__) + + +if ServerlessModeProtocol is not None: + # New Relic Agent >=5.16 + def protocol_finalize(self): + for key in self.configuration.aws_lambda_metadata: + if key not in self._metadata: + self._metadata[key] = self.configuration.aws_lambda_metadata[key] + + data = self.client.finalize() + + payload = { + "metadata": self._metadata, + "data": data, + } + + encoded = serverless_payload_encode(payload) + payload = json_encode((1, "NR_LAMBDA_MONITORING", encoded)) + + if os.path.exists(NAMED_PIPE_PATH): + try: + with open(NAMED_PIPE_PATH, "w") as named_pipe: + named_pipe.write(payload) + except IOError as e: + logger.error( + "Failed to write to named pipe %s: %s" % (NAMED_PIPE_PATH, e) + ) + else: + print(payload) + + return payload + + ServerlessModeProtocol.finalize = protocol_finalize + +else: + # New Relic Agent <5.16 + def session_finalize(self): + encoded = serverless_payload_encode(self.payload) + payload = json_encode((1, "NR_LAMBDA_MONITORING", encoded)) + + if os.path.exists(NAMED_PIPE_PATH): + try: + with open(NAMED_PIPE_PATH, "w") as named_pipe: + named_pipe.write(payload) + except IOError as e: + logger.error( + "Failed to write to named pipe %s: %s" % (NAMED_PIPE_PATH, e) + ) + else: + print(payload) + + # Clear data after sending + self._data.clear() + return payload + + ServerlessModeSession.finalize = session_finalize diff --git a/python/newrelic_lambda/event-sources.json b/python/newrelic_lambda/event-sources.json new file mode 100644 index 00000000..d2f48e12 --- /dev/null +++ b/python/newrelic_lambda/event-sources.json @@ -0,0 +1,128 @@ +{ + "alb": { + "attributes": {}, + "name": "alb", + "required_keys": [ + "httpMethod", + "requestContext.elb" + ] + }, + "apiGateway": { + "attributes": { + "aws.lambda.eventSource.accountId": "requestContext.accountId", + "aws.lambda.eventSource.apiId": "requestContext.apiId", + "aws.lambda.eventSource.resourceId": "requestContext.resourceId", + "aws.lambda.eventSource.resourcePath": "requestContext.resourcePath", + "aws.lambda.eventSource.stage": "requestContext.stage" + }, + "name": "apiGateway", + "required_keys": [ + "headers", + "httpMethod", + "path", + "requestContext", + "requestContext.stage" + ] + }, + "cloudFront": { + "attributes": {}, + "name": "cloudFront", + "required_keys": [ + "Records[0].cf" + ] + }, + "cloudWatchScheduled": { + "attributes": { + "aws.lambda.eventSource.account": "account", + "aws.lambda.eventSource.id": "id", + "aws.lambda.eventSource.region": "region", + "aws.lambda.eventSource.resource": "resources[0]", + "aws.lambda.eventSource.time": "time" + }, + "name": "cloudWatch_scheduled", + "required_keys": [ + "detail-type", + "source" + ] + }, + "dynamoStreams": { + "attributes": { + "aws.lambda.eventSource.length": "Records.length" + }, + "name": "dynamo_streams", + "required_keys": [ + "Records[0].dynamodb" + ] + }, + "firehose": { + "attributes": { + "aws.lambda.eventSource.length": "records.length", + "aws.lambda.eventSource.region": "region" + }, + "name": "firehose", + "required_keys": [ + "deliveryStreamArn", + "records[0].kinesisRecordMetadata" + ] + }, + "kinesis": { + "attributes": { + "aws.lambda.eventSource.length": "Records.length", + "aws.lambda.eventSource.region": "Records[0].awsRegion" + }, + "name": "kinesis", + "required_keys": [ + "Records[0].kinesis" + ] + }, + "s3": { + "attributes": { + "aws.lambda.eventSource.bucketName": "Records[0].s3.bucket.name", + "aws.lambda.eventSource.eventName": "Records[0].eventName", + "aws.lambda.eventSource.eventTime": "Records[0].eventTime", + "aws.lambda.eventSource.length": "Records.length", + "aws.lambda.eventSource.objectKey": "Records[0].s3.object.key", + "aws.lambda.eventSource.objectSequencer": "Records[0].s3.object.sequencer", + "aws.lambda.eventSource.objectSize": "Records[0].s3.object.size", + "aws.lambda.eventSource.region": "Records[0].awsRegion" + }, + "name": "s3", + "required_keys": [ + "Records[0].s3" + ] + }, + "ses": { + "attributes": { + "aws.lambda.eventSource.date": "Records[0].ses.mail.commonHeaders.date", + "aws.lambda.eventSource.length": "Records.length", + "aws.lambda.eventSource.messageId": "Records[0].ses.mail.commonHeaders.messageId", + "aws.lambda.eventSource.returnPath": "Records[0].ses.mail.commonHeaders.returnPath" + }, + "name": "ses", + "required_keys": [ + "Records[0].ses" + ] + }, + "sns": { + "attributes": { + "aws.lambda.eventSource.length": "Records.length", + "aws.lambda.eventSource.messageId": "Records[0].Sns.MessageId", + "aws.lambda.eventSource.timestamp": "Records[0].Sns.Timestamp", + "aws.lambda.eventSource.topicArn": "Records[0].Sns.TopicArn", + "aws.lambda.eventSource.type": "Records[0].Sns.Type" + }, + "name": "sns", + "required_keys": [ + "Records[0].Sns" + ] + }, + "sqs": { + "attributes": { + "aws.lambda.eventSource.length": "Records.length" + }, + "name": "sqs", + "required_keys": [ + "Records[0].receiptHandle" + ] + } +} \ No newline at end of file diff --git a/python/newrelic_lambda/lambda_handler.py b/python/newrelic_lambda/lambda_handler.py new file mode 100644 index 00000000..dd653242 --- /dev/null +++ b/python/newrelic_lambda/lambda_handler.py @@ -0,0 +1,274 @@ +import functools +import json +import os +import re + +import newrelic.agent +import newrelic.core.attribute + +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode + +# noinspection PyProtectedMember +newrelic.core.attribute._TRANSACTION_EVENT_DEFAULT_ATTRIBUTES.update( + { + "aws.lambda.eventSource.account", + "aws.lambda.eventSource.accountId", + "aws.lambda.eventSource.apiId", + "aws.lambda.eventSource.bucketName", + "aws.lambda.eventSource.date", + "aws.lambda.eventSource.eventName", + "aws.lambda.eventSource.eventTime", + "aws.lambda.eventSource.eventType", + "aws.lambda.eventSource.id", + "aws.lambda.eventSource.length", + "aws.lambda.eventSource.messageId", + "aws.lambda.eventSource.objectKey", + "aws.lambda.eventSource.objectSequencer", + "aws.lambda.eventSource.objectSize", + "aws.lambda.eventSource.region", + "aws.lambda.eventSource.resource", + "aws.lambda.eventSource.resourceId", + "aws.lambda.eventSource.resourcePath", + "aws.lambda.eventSource.returnPath", + "aws.lambda.eventSource.stage", + "aws.lambda.eventSource.time", + "aws.lambda.eventSource.timestamp", + "aws.lambda.eventSource.topicArn", + "aws.lambda.eventSource.type", + "aws.lambda.eventSource.xAmzId2", + "aws.lambda.functionVersion", + "request.headers.host", + } +) + +COLD_START_RECORDED = False +MEGABYTE_IN_BYTES = 2 ** 20 +PATH_SPLIT_REGEX = re.compile(r"[.\[]") + +# We're using JSON here to maximize cross-agent consistency. +with open(os.path.join(os.path.dirname(__file__), "event-sources.json")) as f: + EVENT_TYPE_INFO = json.load(f) + + +def path_match(path, obj): + return path_get(path, obj) is not None + + +def path_get(path, obj): + path = PATH_SPLIT_REGEX.split(path) + + pos = obj + for segment in path: + segment = segment.rstrip("]") + try: + if segment.isdigit(): + segment = int(segment) + elif segment == "length": + return len(pos) + pos = pos[segment] + except IndexError: + return None + except KeyError: + return None + return pos + + +def extract_event_source_arn(event): + try: + # Firehose + arn = event.get("streamArn") or event.get("deliveryStreamArn") + + if not arn: + # Dynamo, Kinesis, S3, SNS, SQS + record = path_get("Records[0]", event) + + if record: + arn = ( + record.get("eventSourceARN") + or record.get("EventSubscriptionArn") + or path_get("s3.bucket.arn", record) + ) + # ALB + if not arn: + arn = path_get("requestContext.elb.targetGroupArn", event) + # CloudWatch events + if not arn: + arn = path_get("resources[0]", event) + + if arn: + return newrelic.core.attribute.truncate(str(arn)) + return None + except Exception: + pass + + +def detect_event_type(event): + if isinstance(event, dict): + for k, type_info in EVENT_TYPE_INFO.items(): + if all(path_match(path, event) for path in type_info["required_keys"]): + return type_info + return None + + +def get_attributes_for_event_type(event_type, event): + attr_names_and_values = {} + event_type_attributes = event_type["attributes"] + for attr_name, path in event_type_attributes.items(): + attr = path_get(path, event) + if attr is not None: + attr_names_and_values[attr_name] = attr + return attr_names_and_values + + +def LambdaHandlerWrapper(wrapped, application=None, name=None, group=None): + def set_agent_attr(transaction, key, value): + # noinspection PyProtectedMember + transaction._add_agent_attribute(key, value) + + def _nr_lambda_handler_wrapper_(wrapped, instance, args, kwargs): + # Check to see if any transaction is present, even an inactive + # one which has been marked to be ignored or which has been + # stopped already. + + transaction = newrelic.agent.current_transaction(active_only=False) + + if transaction: + return wrapped(*args, **kwargs) + + try: + event, context = args[:2] + except Exception: + return wrapped(*args, **kwargs) + + target_application = application + + # If application has an activate() method we assume it is an + # actual application. Do this rather than check type so that + # can easily mock it for testing. + + # FIXME Should this allow for multiple apps if a string. + + if not hasattr(application, "activate"): + target_application = newrelic.agent.application(application) + + try: + if "httpMethod" in event: + request_method = event["httpMethod"] + request_path = event["path"] + else: + request_method = event["requestContext"]["http"]["method"] + request_path = event["requestContext"]["http"]["path"] + + headers = None + if "headers" in event: + headers = event["headers"] + elif "multiValueHeaders" in event: + headers = { + k: ", ".join(v) for k, v in event["multiValueHeaders"].items() + } + background_task = False + try: + query_string = None + if "queryStringParameters" in event: + query_string = urlencode(event["queryStringParameters"], True) + elif "multiValueQueryStringParameters" in event: + query_string = urlencode( + event["multiValueQueryStringParameters"], True + ) + except Exception: + query_string = None + except Exception: + request_method = None + request_path = None + headers = None + query_string = None + background_task = True + + transaction_name = name or getattr(context, "function_name", None) + + transaction = newrelic.agent.WebTransaction( + target_application, + transaction_name, + group=group, + request_method=request_method, + request_path=request_path, + headers=headers, + query_string=query_string, + ) + + transaction.background_task = background_task + + request_id = getattr(context, "aws_request_id", None) + aws_arn = getattr(context, "invoked_function_arn", None) + function_version = getattr(context, "function_version", None) + event_source = extract_event_source_arn(event) + event_type = detect_event_type(event) + + if request_id: + set_agent_attr(transaction, "aws.requestId", request_id) + if aws_arn: + set_agent_attr(transaction, "aws.lambda.arn", aws_arn) + if function_version: + set_agent_attr(transaction, "aws.lambda.functionVersion", function_version) + if event_source: + set_agent_attr(transaction, "aws.lambda.eventSource.arn", event_source) + if event_type: + event_type_name = event_type["name"] + set_agent_attr( + transaction, "aws.lambda.eventSource.eventType", event_type_name + ) + + # Save event-specific attributes + for attr_name, attr in get_attributes_for_event_type( + event_type, event + ).items(): + set_agent_attr(transaction, attr_name, attr) + + # COLD_START_RECORDED is initialized to "False" when the container + # first starts up, and will remain that way until the below lines + # of code are encountered during the first transaction after the cold + # start. We record this occurence on the transaction so that an + # attribute is created, and then set COLD_START_RECORDED to False so + # that the attribute is not created again during future invocations of + # this container. + + global COLD_START_RECORDED + if COLD_START_RECORDED is False: + set_agent_attr(transaction, "aws.lambda.coldStart", True) + COLD_START_RECORDED = True + + settings = newrelic.agent.global_settings() + if aws_arn: + settings.aws_lambda_metadata["arn"] = aws_arn + if function_version: + settings.aws_lambda_metadata["function_version"] = function_version + + with transaction: + result = wrapped(*args, **kwargs) + + if not background_task: + try: + status_code = result.get("statusCode") + response_headers = result.get("headers") + + try: + response_headers = response_headers.items() + except Exception: + response_headers = None + + transaction.process_response(status_code, response_headers) + except Exception: + pass + + return result + + return newrelic.agent.FunctionWrapper(wrapped, _nr_lambda_handler_wrapper_) + + +def lambda_handler(application=None, name=None, group=None): + return functools.partial( + LambdaHandlerWrapper, application=application, name=name, group=group + ) diff --git a/python/publish-layers.sh b/python/publish-layers.sh index d294d984..2ce810cc 100755 --- a/python/publish-layers.sh +++ b/python/publish-layers.sh @@ -29,8 +29,9 @@ function build-python37-x86 { echo "Building New Relic layer for python3.7 (x86_64)" rm -rf $BUILD_DIR $PY37_DIST_X86_64 mkdir -p $DIST_DIR - pip install --no-cache-dir -qU newrelic newrelic-lambda -t $BUILD_DIR/lib/python3.7/site-packages + pip install --no-cache-dir -qU newrelic -t $BUILD_DIR/lib/python3.7/site-packages cp newrelic_lambda_wrapper.py $BUILD_DIR/lib/python3.7/site-packages/newrelic_lambda_wrapper.py + cp -r newrelic_lambda $BUILD_DIR/lib/python3.7/site-packages/newrelic_lambda find $BUILD_DIR -name '__pycache__' -exec rm -rf {} + download_extension x86_64 zip -rq $PY37_DIST_X86_64 $BUILD_DIR $EXTENSION_DIST_DIR $EXTENSION_DIST_PREVIEW_FILE @@ -53,8 +54,9 @@ function build-python38-arm64 { echo "Building New Relic layer for python3.8 (arm64)" rm -rf $BUILD_DIR $PY38_DIST_ARM64 mkdir -p $DIST_DIR - pip install --no-cache-dir -qU newrelic newrelic-lambda -t $BUILD_DIR/lib/python3.8/site-packages + pip install --no-cache-dir -qU newrelic -t $BUILD_DIR/lib/python3.8/site-packages cp newrelic_lambda_wrapper.py $BUILD_DIR/lib/python3.8/site-packages/newrelic_lambda_wrapper.py + cp -r newrelic_lambda $BUILD_DIR/lib/python3.8/site-packages/newrelic_lambda find $BUILD_DIR -name '__pycache__' -exec rm -rf {} + download_extension arm64 zip -rq $PY38_DIST_ARM64 $BUILD_DIR $EXTENSION_DIST_DIR $EXTENSION_DIST_PREVIEW_FILE @@ -66,8 +68,9 @@ function build-python38-x86 { echo "Building New Relic layer for python3.8 (x86_64)" rm -rf $BUILD_DIR $PY38_DIST_X86_64 mkdir -p $DIST_DIR - pip install --no-cache-dir -qU newrelic newrelic-lambda -t $BUILD_DIR/lib/python3.8/site-packages + pip install --no-cache-dir -qU newrelic -t $BUILD_DIR/lib/python3.8/site-packages cp newrelic_lambda_wrapper.py $BUILD_DIR/lib/python3.8/site-packages/newrelic_lambda_wrapper.py + cp -r newrelic_lambda $BUILD_DIR/lib/python3.8/site-packages/newrelic_lambda find $BUILD_DIR -name '__pycache__' -exec rm -rf {} + download_extension x86_64 zip -rq $PY38_DIST_X86_64 $BUILD_DIR $EXTENSION_DIST_DIR $EXTENSION_DIST_PREVIEW_FILE @@ -101,8 +104,9 @@ function build-python39-arm64 { echo "Building New Relic layer for python3.9 (arm64)" rm -rf $BUILD_DIR $PY39_DIST_ARM64 mkdir -p $DIST_DIR - pip install --no-cache-dir -qU newrelic newrelic-lambda -t $BUILD_DIR/lib/python3.9/site-packages + pip install --no-cache-dir -qU newrelic -t $BUILD_DIR/lib/python3.9/site-packages cp newrelic_lambda_wrapper.py $BUILD_DIR/lib/python3.9/site-packages/newrelic_lambda_wrapper.py + cp -r newrelic_lambda $BUILD_DIR/lib/python3.9/site-packages/newrelic_lambda find $BUILD_DIR -name '__pycache__' -exec rm -rf {} + download_extension arm64 zip -rq $PY39_DIST_ARM64 $BUILD_DIR $EXTENSION_DIST_DIR $EXTENSION_DIST_PREVIEW_FILE @@ -114,8 +118,9 @@ function build-python39-x86 { echo "Building New Relic layer for python3.9 (x86_64)" rm -rf $BUILD_DIR $PY39_DIST_X86_64 mkdir -p $DIST_DIR - pip install --no-cache-dir -qU newrelic newrelic-lambda -t $BUILD_DIR/lib/python3.9/site-packages + pip install --no-cache-dir -qU newrelic -t $BUILD_DIR/lib/python3.9/site-packages cp newrelic_lambda_wrapper.py $BUILD_DIR/lib/python3.9/site-packages/newrelic_lambda_wrapper.py + cp -r newrelic_lambda $BUILD_DIR/lib/python3.9/site-packages/newrelic_lambda find $BUILD_DIR -name '__pycache__' -exec rm -rf {} + download_extension x86_64 zip -rq $PY39_DIST_X86_64 $BUILD_DIR $EXTENSION_DIST_DIR $EXTENSION_DIST_PREVIEW_FILE @@ -149,8 +154,9 @@ function build-python310-arm64 { echo "Building New Relic layer for python3.10 (arm64)" rm -rf $BUILD_DIR $PY310_DIST_ARM64 mkdir -p $DIST_DIR - pip install --no-cache-dir -qU newrelic newrelic-lambda -t $BUILD_DIR/lib/python3.10/site-packages + pip install --no-cache-dir -qU newrelic -t $BUILD_DIR/lib/python3.10/site-packages cp newrelic_lambda_wrapper.py $BUILD_DIR/lib/python3.10/site-packages/newrelic_lambda_wrapper.py + cp -r newrelic_lambda $BUILD_DIR/lib/python3.10/site-packages/newrelic_lambda find $BUILD_DIR -name '__pycache__' -exec rm -rf {} + download_extension arm64 zip -rq $PY310_DIST_ARM64 $BUILD_DIR $EXTENSION_DIST_DIR $EXTENSION_DIST_PREVIEW_FILE @@ -162,8 +168,9 @@ function build-python310-x86 { echo "Building New Relic layer for python3.10 (x86_64)" rm -rf $BUILD_DIR $PY310_DIST_X86_64 mkdir -p $DIST_DIR - pip install --no-cache-dir -qU newrelic newrelic-lambda -t $BUILD_DIR/lib/python3.10/site-packages + pip install --no-cache-dir -qU newrelic -t $BUILD_DIR/lib/python3.10/site-packages cp newrelic_lambda_wrapper.py $BUILD_DIR/lib/python3.10/site-packages/newrelic_lambda_wrapper.py + cp -r newrelic_lambda $BUILD_DIR/lib/python3.10/site-packages/newrelic_lambda find $BUILD_DIR -name '__pycache__' -exec rm -rf {} + download_extension x86_64 zip -rq $PY310_DIST_X86_64 $BUILD_DIR $EXTENSION_DIST_DIR $EXTENSION_DIST_PREVIEW_FILE @@ -197,8 +204,9 @@ function build-python311-arm64 { echo "Building New Relic layer for python3.11 (arm64)" rm -rf $BUILD_DIR $PY311_DIST_ARM64 mkdir -p $DIST_DIR - pip install --no-cache-dir -qU newrelic newrelic-lambda -t $BUILD_DIR/lib/python3.11/site-packages + pip install --no-cache-dir -qU newrelic -t $BUILD_DIR/lib/python3.11/site-packages cp newrelic_lambda_wrapper.py $BUILD_DIR/lib/python3.11/site-packages/newrelic_lambda_wrapper.py + cp -r newrelic_lambda $BUILD_DIR/lib/python3.11/site-packages/newrelic_lambda find $BUILD_DIR -name '__pycache__' -exec rm -rf {} + download_extension arm64 zip -rq $PY311_DIST_ARM64 $BUILD_DIR $EXTENSION_DIST_DIR $EXTENSION_DIST_PREVIEW_FILE @@ -210,8 +218,9 @@ function build-python311-x86 { echo "Building New Relic layer for python3.11 (x86_64)" rm -rf $BUILD_DIR $PY311_DIST_X86_64 mkdir -p $DIST_DIR - pip install --no-cache-dir -qU newrelic newrelic-lambda -t $BUILD_DIR/lib/python3.11/site-packages + pip install --no-cache-dir -qU newrelic -t $BUILD_DIR/lib/python3.11/site-packages cp newrelic_lambda_wrapper.py $BUILD_DIR/lib/python3.11/site-packages/newrelic_lambda_wrapper.py + cp -r newrelic_lambda $BUILD_DIR/lib/python3.11/site-packages/newrelic_lambda find $BUILD_DIR -name '__pycache__' -exec rm -rf {} + download_extension x86_64 zip -rq $PY311_DIST_X86_64 $BUILD_DIR $EXTENSION_DIST_DIR $EXTENSION_DIST_PREVIEW_FILE @@ -245,8 +254,9 @@ function build-python312-arm64 { echo "Building New Relic layer for python3.12 (arm64)" rm -rf $BUILD_DIR $PY312_DIST_ARM64 mkdir -p $DIST_DIR - pip install --no-cache-dir -qU newrelic newrelic-lambda -t $BUILD_DIR/lib/python3.12/site-packages + pip install --no-cache-dir -qU newrelic -t $BUILD_DIR/lib/python3.12/site-packages cp newrelic_lambda_wrapper.py $BUILD_DIR/lib/python3.12/site-packages/newrelic_lambda_wrapper.py + cp -r newrelic_lambda $BUILD_DIR/lib/python3.12/site-packages/newrelic_lambda find $BUILD_DIR -name '__pycache__' -exec rm -rf {} + download_extension arm64 zip -rq $PY312_DIST_ARM64 $BUILD_DIR $EXTENSION_DIST_DIR $EXTENSION_DIST_PREVIEW_FILE @@ -258,8 +268,9 @@ function build-python312-x86 { echo "Building New Relic layer for python3.12 (x86_64)" rm -rf $BUILD_DIR $PY312_DIST_X86_64 mkdir -p $DIST_DIR - pip install --no-cache-dir -qU newrelic newrelic-lambda -t $BUILD_DIR/lib/python3.12/site-packages + pip install --no-cache-dir -qU newrelic -t $BUILD_DIR/lib/python3.12/site-packages cp newrelic_lambda_wrapper.py $BUILD_DIR/lib/python3.12/site-packages/newrelic_lambda_wrapper.py + cp -r newrelic_lambda $BUILD_DIR/lib/python3.12/site-packages/newrelic_lambda find $BUILD_DIR -name '__pycache__' -exec rm -rf {} + download_extension x86_64 zip -rq $PY312_DIST_X86_64 $BUILD_DIR $EXTENSION_DIST_DIR $EXTENSION_DIST_PREVIEW_FILE