From 2a20f4ddeac467567210d199d8a1b1b4d6c95106 Mon Sep 17 00:00:00 2001 From: "(Eliseo) Nathaniel Ruiz Nowell" Date: Mon, 25 Oct 2021 15:32:17 -0400 Subject: [PATCH] Add AWS Lambda package metadata files --- .../LICENSE | 0 .../MANIFEST.in | 0 .../README.rst | 0 .../setup.cfg | 0 .../setup.py | 0 .../instrumentation/aws_lambda/__init__.py | 13 + .../instrumentation/aws_lambda/package.py | 0 .../instrumentation/aws_lambda/version.py | 0 .../tests/test_aws_lambda_instrumentation.py} | 4 - .../scripts/otel-instrument | 166 -------- .../scripts/otel_wrapper.py | 66 --- .../instrumentation/aws_lambda/__init__.py | 263 ------------ .../tests/test_aws_lambda_instrumentation.py | 376 ------------------ tox.ini | 7 - 14 files changed, 13 insertions(+), 882 deletions(-) rename instrumentation/{opentelemetry-instrumentation-aws_lambda => opentelemetry-instrumentation-aws-lambda}/LICENSE (100%) rename instrumentation/{opentelemetry-instrumentation-aws_lambda => opentelemetry-instrumentation-aws-lambda}/MANIFEST.in (100%) rename instrumentation/{opentelemetry-instrumentation-aws_lambda => opentelemetry-instrumentation-aws-lambda}/README.rst (100%) rename instrumentation/{opentelemetry-instrumentation-aws_lambda => opentelemetry-instrumentation-aws-lambda}/setup.cfg (100%) rename instrumentation/{opentelemetry-instrumentation-aws_lambda => opentelemetry-instrumentation-aws-lambda}/setup.py (100%) create mode 100644 instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py rename instrumentation/{opentelemetry-instrumentation-aws_lambda => opentelemetry-instrumentation-aws-lambda}/src/opentelemetry/instrumentation/aws_lambda/package.py (100%) rename instrumentation/{opentelemetry-instrumentation-aws_lambda => opentelemetry-instrumentation-aws-lambda}/src/opentelemetry/instrumentation/aws_lambda/version.py (100%) rename instrumentation/{opentelemetry-instrumentation-aws_lambda/tests/mocks/lambda_function.py => opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation.py} (91%) delete mode 100755 instrumentation/opentelemetry-instrumentation-aws_lambda/scripts/otel-instrument delete mode 100644 instrumentation/opentelemetry-instrumentation-aws_lambda/scripts/otel_wrapper.py delete mode 100644 instrumentation/opentelemetry-instrumentation-aws_lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py delete mode 100644 instrumentation/opentelemetry-instrumentation-aws_lambda/tests/test_aws_lambda_instrumentation.py diff --git a/instrumentation/opentelemetry-instrumentation-aws_lambda/LICENSE b/instrumentation/opentelemetry-instrumentation-aws-lambda/LICENSE similarity index 100% rename from instrumentation/opentelemetry-instrumentation-aws_lambda/LICENSE rename to instrumentation/opentelemetry-instrumentation-aws-lambda/LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-aws_lambda/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-aws-lambda/MANIFEST.in similarity index 100% rename from instrumentation/opentelemetry-instrumentation-aws_lambda/MANIFEST.in rename to instrumentation/opentelemetry-instrumentation-aws-lambda/MANIFEST.in diff --git a/instrumentation/opentelemetry-instrumentation-aws_lambda/README.rst b/instrumentation/opentelemetry-instrumentation-aws-lambda/README.rst similarity index 100% rename from instrumentation/opentelemetry-instrumentation-aws_lambda/README.rst rename to instrumentation/opentelemetry-instrumentation-aws-lambda/README.rst diff --git a/instrumentation/opentelemetry-instrumentation-aws_lambda/setup.cfg b/instrumentation/opentelemetry-instrumentation-aws-lambda/setup.cfg similarity index 100% rename from instrumentation/opentelemetry-instrumentation-aws_lambda/setup.cfg rename to instrumentation/opentelemetry-instrumentation-aws-lambda/setup.cfg diff --git a/instrumentation/opentelemetry-instrumentation-aws_lambda/setup.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/setup.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-aws_lambda/setup.py rename to instrumentation/opentelemetry-instrumentation-aws-lambda/setup.py diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py new file mode 100644 index 0000000000..6ab2e961ec --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. diff --git a/instrumentation/opentelemetry-instrumentation-aws_lambda/src/opentelemetry/instrumentation/aws_lambda/package.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/package.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-aws_lambda/src/opentelemetry/instrumentation/aws_lambda/package.py rename to instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/package.py diff --git a/instrumentation/opentelemetry-instrumentation-aws_lambda/src/opentelemetry/instrumentation/aws_lambda/version.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/version.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-aws_lambda/src/opentelemetry/instrumentation/aws_lambda/version.py rename to instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/version.py diff --git a/instrumentation/opentelemetry-instrumentation-aws_lambda/tests/mocks/lambda_function.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation.py similarity index 91% rename from instrumentation/opentelemetry-instrumentation-aws_lambda/tests/mocks/lambda_function.py rename to instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation.py index c292575651..b0a6f42841 100644 --- a/instrumentation/opentelemetry-instrumentation-aws_lambda/tests/mocks/lambda_function.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation.py @@ -11,7 +11,3 @@ # 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. - - -def handler(event, context): - return "200 ok" diff --git a/instrumentation/opentelemetry-instrumentation-aws_lambda/scripts/otel-instrument b/instrumentation/opentelemetry-instrumentation-aws_lambda/scripts/otel-instrument deleted file mode 100755 index 41c248b5d7..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aws_lambda/scripts/otel-instrument +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -""" -`otel-instrument` - -This script configures and sets up OpenTelemetry Python with the values we -expect will be used by the common user. It does this by setting the environment -variables OpenTelemetry uses, and then initializing OpenTelemetry using the -`opentelemetry-instrument` auto instrumentation script from the -`opentelemetry-instrumentation` package. - -Additionally, this configuration assumes the user is using packages conforming -to the `opentelemetry-instrumentation` and `opentelemetry-sdk` specifications. - -DO NOT use this script for anything else besides SETTING ENVIRONMENT VARIABLES. - -Usage ------ -We expect this file to be at the root of a Lambda Layer. Having it anywhere else -seems to mean AWS Lambda cannot find it. - -In the configuration of an AWS Lambda function with this file at the -root level of a Lambda Layer: - -.. code:: - - AWS_LAMBDA_EXEC_WRAPPER = /opt/otel-instrument - -""" - -import os -import sys -from os import environ, system - -# Use constants to access the environment variables we want to use in this -# script. - -# See more: -# https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime - -# - Reserved environment variables - -AWS_LAMBDA_FUNCTION_NAME = "AWS_LAMBDA_FUNCTION_NAME" -LAMBDA_RUNTIME_DIR = "LAMBDA_RUNTIME_DIR" - -# - Unreserved environment variables - -PYTHONPATH = "PYTHONPATH" - -# Update the python paths for packages with `sys.path` and `PYTHONPATH` - -# - We know that the path to the Lambda Layer OpenTelemetry Python packages are -# well defined, so we can add them to the PYTHONPATH. -# -# See more: -# https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html#configuration-layers-path - -LAMBDA_LAYER_PKGS_DIR = os.path.abspath(os.path.join(os.sep, "opt", "python")) - -# - Set Lambda Layer python packages in PYTHONPATH so `opentelemetry-instrument` -# script can find them (it needs to find `opentelemetry` to find the auto -# instrumentation `run()` method later) - -if PYTHONPATH not in environ: - environ[PYTHONPATH] = LAMBDA_LAYER_PKGS_DIR -elif LAMBDA_LAYER_PKGS_DIR not in environ[PYTHONPATH]: - environ[PYTHONPATH] += os.pathsep + LAMBDA_LAYER_PKGS_DIR - -# - Set Lambda runtime python packages in PYTHONPATH so -# `opentelemetry-instrument` script can find them during auto instrumentation -# and instrument them. - -if PYTHONPATH not in environ: - environ[PYTHONPATH] = os.environ[LAMBDA_RUNTIME_DIR] -if os.environ[LAMBDA_RUNTIME_DIR] not in environ[PYTHONPATH]: - environ[PYTHONPATH] += os.pathsep + os.environ[LAMBDA_RUNTIME_DIR] - -# - Set Lambda Layer python packages in current python path so we can find them -# right away in this script - -if LAMBDA_LAYER_PKGS_DIR not in sys.path: - sys.path.append(LAMBDA_LAYER_PKGS_DIR) - -# Configure OpenTelemetry Python with environment variables - -from opentelemetry.environment_variables import ( - OTEL_PROPAGATORS, - OTEL_TRACES_EXPORTER, -) -from opentelemetry.sdk.environment_variables import ( - OTEL_RESOURCE_ATTRIBUTES, - OTEL_SERVICE_NAME, -) - -# - Set the default Trace Exporter - -environ.setdefault(OTEL_TRACES_EXPORTER, "otlp_proto_grpc_span") - -# - Set the service name - -environ.setdefault(OTEL_SERVICE_NAME, environ.get(AWS_LAMBDA_FUNCTION_NAME)) - -# - Set the Resource Detectors (Resource Attributes) -# -# TODO: waiting on OTel Python support for configuring Resource Detectors from -# an environment variable. Replace the bottom code with the following when -# this is possible. -# -# environ["OTEL_RESOURCE_DETECTORS"] = "aws_lambda" -# -lambda_resource_attributes = ( - "cloud.region=%s,cloud.provider=aws,faas.name=%s,faas.version=%s" - % ( - environ.get("AWS_REGION"), - environ.get(AWS_LAMBDA_FUNCTION_NAME), - environ.get("AWS_LAMBDA_FUNCTION_VERSION"), - ) -) - -if OTEL_RESOURCE_ATTRIBUTES not in environ: - environ[OTEL_RESOURCE_ATTRIBUTES] = lambda_resource_attributes -else: - environ[OTEL_RESOURCE_ATTRIBUTES] = "%s,%s" % ( - lambda_resource_attributes, - environ.get(OTEL_RESOURCE_ATTRIBUTES), - ) - -# - Set the default propagators - -environ.setdefault(OTEL_PROPAGATORS, "tracecontext,b3,xray") - -# - Use a wrapper because AWS Lambda's `python3 /var/runtime/bootstrap.py` will -# use `imp.load_module` to load the function from the `_HANDLER` environment -# variable. This RELOADS the module and REMOVES any instrumentation patching -# done earlier. So we delay instrumentation until `boostrap.py` imports -# `otel_wrapper.py` at which we know the patching will be picked up. -# -# See more: -# https://docs.python.org/3/library/imp.html#imp.load_module - -environ["ORIG_HANDLER"] = environ.get("_HANDLER") -environ["_HANDLER"] = "otel_wrapper.lambda_handler" - -# - Call the upstream auto instrumentation script - -system( - sys.argv[1] + - " " + - os.path.join(LAMBDA_LAYER_PKGS_DIR, "bin", "opentelemetry-instrument",) + - " " + - " ".join(sys.argv[1:]) -) diff --git a/instrumentation/opentelemetry-instrumentation-aws_lambda/scripts/otel_wrapper.py b/instrumentation/opentelemetry-instrumentation-aws_lambda/scripts/otel_wrapper.py deleted file mode 100644 index 7883f714cc..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aws_lambda/scripts/otel_wrapper.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -""" -`otel_wrapper.py` - -This file serves as a wrapper over the user's Lambda function. - -Usage ------ -Patch the reserved `_HANDLER` Lambda environment variable to point to this -file's `otel_wrapper.lambda_handler` property. Do this having saved the original -`_HANDLER` in the `ORIG_HANDLER` environment variable. Doing this makes it so -that **on import of this file, the handler is instrumented**. - -Instrumenting any earlier will cause the instrumentation to be lost because the -AWS Service uses `imp.load_module` to import the handler which RELOADS the -module. This is why AwsLambdaInstrumentor cannot be instrumented with the -`opentelemetry-instrument` script. - -See more: -https://docs.python.org/3/library/imp.html#imp.load_module - -""" - -import os -from importlib import import_module - -from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor - -AwsLambdaInstrumentor().instrument() - - -def modify_module_name(module_name): - """Returns a valid modified module to get imported""" - return ".".join(module_name.split("/")) - - -class HandlerError(Exception): - pass - - -path = os.environ.get("ORIG_HANDLER", None) -if path is None: - raise HandlerError("ORIG_HANDLER is not defined.") -parts = path.rsplit(".", 1) -if len(parts) != 2: - raise HandlerError("Value %s for ORIG_HANDLER has invalid format." % path) - -(mod_name, handler_name) = parts -modified_mod_name = modify_module_name(mod_name) -handler_module = import_module(modified_mod_name) -lambda_handler = getattr(handler_module, handler_name) diff --git a/instrumentation/opentelemetry-instrumentation-aws_lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py b/instrumentation/opentelemetry-instrumentation-aws_lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py deleted file mode 100644 index e22f66ada1..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aws_lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py +++ /dev/null @@ -1,263 +0,0 @@ -# Copyright 2020, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -""" -The opentelemetry-instrumentation-aws-lambda package provides an `Instrumentor` -to traces calls whithin a Python AWS Lambda function. - -Usage ------ - -.. code:: python - - # Copy this snippet into an AWS Lambda function - - import boto3 - from opentelemetry.instrumentation.botocore import AwsBotocoreInstrumentor - from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor - - - # Enable instrumentation - AwsBotocoreInstrumentor().instrument() - AwsLambdaInstrumentor().instrument() - - # Lambda function - def lambda_handler(event, context): - s3 = boto3.resource('s3') - for bucket in s3.buckets.all(): - print(bucket.name) - - return "200 OK" - -API ---- - -The `instrument` method accepts the following keyword args: - -tracer_provider (TracerProvider) - an optional tracer provider -event_context_extractor (Callable) - a function that returns an OTel Trace - Context given the Lambda Event the AWS Lambda was invoked with - this function signature is: def event_context_extractor(lambda_event: Any) -> Context -for example: - -.. code:: python - - from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor - - def custom_event_context_extractor(lambda_event): - # If the `TraceContextTextMapPropagator` is the global propagator, we - # can use it to parse out the context from the HTTP Headers. - return get_global_textmap().extract(lambda_event["foo"]["headers"]) - - AwsLambdaInstrumentor().instrument( - event_context_extractor=custom_event_context_extractor - ) -""" - -import logging -import os -from importlib import import_module -from typing import Any, Collection - -from wrapt import wrap_function_wrapper - -from opentelemetry.context.context import Context -from opentelemetry.instrumentation.aws_lambda.package import _instruments -from opentelemetry.instrumentation.aws_lambda.version import __version__ -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.utils import unwrap -from opentelemetry.propagate import get_global_textmap -from opentelemetry.propagators.aws.aws_xray_propagator import ( - TRACE_HEADER_KEY, - AwsXRayPropagator, -) -from opentelemetry.semconv.resource import ResourceAttributes -from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace import ( - SpanKind, - TracerProvider, - get_tracer, - get_tracer_provider, -) -from opentelemetry.trace.propagation import get_current_span - -logger = logging.getLogger(__name__) - -_HANDLER = "_HANDLER" -_X_AMZN_TRACE_ID = "_X_AMZN_TRACE_ID" -ORIG_HANDLER = "ORIG_HANDLER" - - -class AwsLambdaInstrumentor(BaseInstrumentor): - def instrumentation_dependencies(self) -> Collection[str]: - return _instruments - - def _instrument(self, **kwargs): - """Instruments Lambda Handlers on AWS Lambda. - - See more: - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#instrumenting-aws-lambda - - Args: - **kwargs: Optional arguments - ``tracer_provider``: a TracerProvider, defaults to global - ``event_context_extractor``: a method which takes the Lambda - Event as input and extracts an OTel Context from it. By default, - the context is extracted from the HTTP headers of an API Gateway - request. - """ - lambda_handler = os.environ.get(ORIG_HANDLER, os.environ.get(_HANDLER)) - # pylint: disable=attribute-defined-outside-init - ( - self._wrapped_module_name, - self._wrapped_function_name, - ) = lambda_handler.rsplit(".", 1) - - _instrument( - self._wrapped_module_name, - self._wrapped_function_name, - tracer_provider=kwargs.get("tracer_provider"), - event_context_extractor=kwargs.get("event_context_extractor"), - ) - - def _uninstrument(self, **kwargs): - unwrap( - import_module(self._wrapped_module_name), - self._wrapped_function_name, - ) - - -def _default_event_context_extractor(lambda_event: Any) -> Context: - """Default way of extracting the context from the Lambda Event. - - Assumes the Lambda Event is a map with the headers under the 'headers' key. - This is the mapping to use when the Lambda is invoked by an API Gateway - REST API where API Gateway is acting as a pure proxy for the request. - - See more: - https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format - - Args: - lambda_event: user-defined, so it could be anything, but this - method counts on it being a map with a 'headers' key - Returns: - A Context with configuration found in the event. - """ - try: - headers = lambda_event["headers"] - except (TypeError, KeyError): - logger.debug( - "Extracting context from Lambda Event failed: either enable X-Ray active tracing or configure API Gateway to trigger this Lambda function as a pure proxy. Otherwise, generated spans will have an invalid (empty) parent context." - ) - headers = {} - return get_global_textmap().extract(headers) - - -def _instrument( - wrapped_module_name, - wrapped_function_name, - tracer_provider: TracerProvider = None, - event_context_extractor=None, -): - def _determine_parent_context(lambda_event: Any) -> Context: - """Determine the parent context for the current Lambda invocation. - - See more: - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#determining-the-parent-of-a-span - - Args: - lambda_event: user-defined, so it could be anything, but this - method counts it being a map with a 'headers' key - Returns: - A Context with configuration found in the carrier. - """ - parent_context = None - - xray_env_var = os.environ.get(_X_AMZN_TRACE_ID) - - if xray_env_var: - parent_context = AwsXRayPropagator().extract( - {TRACE_HEADER_KEY: xray_env_var} - ) - - if ( - parent_context - and get_current_span(parent_context) - .get_span_context() - .trace_flags.sampled - ): - return parent_context - - if event_context_extractor: - parent_context = event_context_extractor(lambda_event) - else: - parent_context = _default_event_context_extractor(lambda_event) - - return parent_context - - def _instrumented_lambda_handler_call( - call_wrapped, instance, args, kwargs - ): - orig_handler_name = ".".join( - [wrapped_module_name, wrapped_function_name] - ) - - lambda_event = args[0] - - parent_context = _determine_parent_context(lambda_event) - - tracer = get_tracer(__name__, __version__, tracer_provider) - - with tracer.start_as_current_span( - name=orig_handler_name, - context=parent_context, - kind=SpanKind.SERVER, - ) as span: - if span.is_recording(): - lambda_context = args[1] - # NOTE: The specs mention an exception here, allowing the - # `ResourceAttributes.FAAS_ID` attribute to be set as a span - # attribute instead of a resource attribute. - # - # See more: - # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/faas.md#example - span.set_attribute( - ResourceAttributes.FAAS_ID, - lambda_context.invoked_function_arn, - ) - span.set_attribute( - SpanAttributes.FAAS_EXECUTION, - lambda_context.aws_request_id, - ) - - result = call_wrapped(*args, **kwargs) - - _tracer_provider = tracer_provider or get_tracer_provider() - if hasattr(_tracer_provider, "force_flush"): - # NOTE: force_flush before function quit in case of Lambda freeze. - # Assumes we are using the OpenTelemetry SDK implementation of the - # `TracerProvider`. - _tracer_provider.force_flush() - else: - logger.error( - "TracerProvider was missing `force_flush` method. This is necessary in case of a Lambda freeze and would exist in the OTel SDK implementation." - ) - - return result - - wrap_function_wrapper( - wrapped_module_name, - wrapped_function_name, - _instrumented_lambda_handler_call, - ) diff --git a/instrumentation/opentelemetry-instrumentation-aws_lambda/tests/test_aws_lambda_instrumentation.py b/instrumentation/opentelemetry-instrumentation-aws_lambda/tests/test_aws_lambda_instrumentation.py deleted file mode 100644 index a2d997dc37..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aws_lambda/tests/test_aws_lambda_instrumentation.py +++ /dev/null @@ -1,376 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -import fileinput -import os -import sys -from importlib import import_module, reload -from shutil import which -from unittest import mock - -from opentelemetry.environment_variables import OTEL_PROPAGATORS -from opentelemetry.instrumentation.aws_lambda import ( - _HANDLER, - _X_AMZN_TRACE_ID, - ORIG_HANDLER, - AwsLambdaInstrumentor, -) -from opentelemetry.propagate import get_global_textmap -from opentelemetry.propagators.aws.aws_xray_propagator import ( - TRACE_ID_FIRST_PART_LENGTH, - TRACE_ID_VERSION, -) -from opentelemetry.semconv.resource import ResourceAttributes -from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.test.test_base import TestBase -from opentelemetry.trace import SpanKind -from opentelemetry.trace.propagation.tracecontext import ( - TraceContextTextMapPropagator, -) - -AWS_LAMBDA_EXEC_WRAPPER = "AWS_LAMBDA_EXEC_WRAPPER" -CONFIGURE_OTEL_SDK_SCRIPTS_DIR = os.path.join( - *(os.path.dirname(__file__), "..", "scripts") -) -TOX_PYTHON_DIRECTORY = os.path.dirname(os.path.dirname(which("python3"))) - - -class MockLambdaContext: - def __init__(self, aws_request_id, invoked_function_arn): - self.invoked_function_arn = invoked_function_arn - self.aws_request_id = aws_request_id - - -MOCK_LAMBDA_CONTEXT = MockLambdaContext( - aws_request_id="mock_aws_request_id", - invoked_function_arn="arn://mock-lambda-function-arn", -) - -MOCK_XRAY_TRACE_ID = 0x5FB7331105E8BB83207FA31D4D9CDB4C -MOCK_XRAY_TRACE_ID_STR = f"{MOCK_XRAY_TRACE_ID:x}" -MOCK_XRAY_PARENT_SPAN_ID = 0x3328B8445A6DBAD2 -MOCK_XRAY_TRACE_CONTEXT_COMMON = f"Root={TRACE_ID_VERSION}-{MOCK_XRAY_TRACE_ID_STR[:TRACE_ID_FIRST_PART_LENGTH]}-{MOCK_XRAY_TRACE_ID_STR[TRACE_ID_FIRST_PART_LENGTH:]};Parent={MOCK_XRAY_PARENT_SPAN_ID:x}" -MOCK_XRAY_TRACE_CONTEXT_SAMPLED = f"{MOCK_XRAY_TRACE_CONTEXT_COMMON};Sampled=1" -MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED = ( - f"{MOCK_XRAY_TRACE_CONTEXT_COMMON};Sampled=0" -) - -# See more: -# https://www.w3.org/TR/trace-context/#examples-of-http-traceparent-headers - -MOCK_W3C_TRACE_ID = 0x5CE0E9A56015FEC5AADFA328AE398115 -MOCK_W3C_PARENT_SPAN_ID = 0xAB54A98CEB1F0AD2 -MOCK_W3C_TRACE_CONTEXT_SAMPLED = ( - f"00-{MOCK_W3C_TRACE_ID:x}-{MOCK_W3C_PARENT_SPAN_ID:x}-01" -) - -MOCK_W3C_TRACE_STATE_KEY = "vendor_specific_key" -MOCK_W3C_TRACE_STATE_VALUE = "test_value" - - -def replace_in_file(file_path, search_text, new_text): - with fileinput.input(file_path, inplace=True) as file_object: - for line in file_object: - new_line = line.replace(search_text, new_text) - # This directs the output to the file, not the console - print(new_line, end="") - - -def mock_aws_lambda_exec_wrapper(): - """Mocks automatically instrumenting user Lambda function by pointing - `AWS_LAMBDA_EXEC_WRAPPER` to the - `otel-instrument` script. - - See more: - https://aws-otel.github.io/docs/getting-started/lambda/lambda-python - """ - - # NOTE: Do NOT run as a sub process because this script needs to update - # the environment variables in this same process. - - original_sys_argv = sys.argv - otel_instrument_file = os.path.join( - CONFIGURE_OTEL_SDK_SCRIPTS_DIR, "otel-instrument" - ) - sys.argv = [ - otel_instrument_file, - which("python3"), - "-c", - "pass", - ] - with open(otel_instrument_file) as config_otel_script: - exec(config_otel_script.read()) - sys.argv = original_sys_argv - - -def mock_execute_lambda(event=None, should_reload=True): - """Mocks the AWS Lambda execution. Like the real Lambda, if - `AWS_LAMBDA_EXEC_WRAPPER` is defined, it calls that script first. - - NOTE: Normally AWS Lambda would give the script the arguments used to start - the program. We don't do that because we want to give the code below which - mocks the `/var/runtime/bootstrap.py` starter file different Lambda event - test cases. We don't want `bootstrap.py` to constrcut them. - - NOTE: We can't use `moto`'s `mock_lambda` because it does not support - AWS_LAMBDA_EXEC_WRAPPER and doesn't mimic the reload behavior we have here. - - See more: - https://docs.aws.amazon.com/lambda/latest/dg/runtimes-modify.html#runtime-wrapper - - Args: - event: The Lambda event which may or may not be used by instrumentation. - should_reload: Whether to reload the import of the module. This is - import for auto-instrumentation tests because they are importing the - same `otel_wrapper.py` file. We skip this for manual-instrumentation - test because otherwise the reload would get rid of the patching done - by `AwsLambdaInstrumentor.()instrument()` in the test case. - """ - if os.environ[AWS_LAMBDA_EXEC_WRAPPER]: - globals()[os.environ[AWS_LAMBDA_EXEC_WRAPPER]]() - - # NOTE: Mocks Lambda's `python3 /var/runtime/bootstrap.py`. Which _reloads_ - # the import of a module using the deprecated `imp.load_module`. This is - # prevents us from simply using `otel-instrument`, and what requires that we - # use `otel_wrapper.py` as well. - # - # See more: - # https://docs.python.org/3/library/imp.html#imp.load_module - - module_name, handler_name = os.environ[_HANDLER].rsplit(".", 1) - handler_module = import_module(module_name.replace("/", ".")) - - if should_reload: - - # NOTE: The first time, this reload produces a `warning` that we are - # "Attempting to instrument while already instrumented". This is fine - # because we are simulating Lambda's "reloading" import so we - # instrument twice. - # - # TODO: (NathanielRN) On subsequent tests, the first import above does - # not run `instrument()` on import. Only `reload` below will run it, no - # warning appears in the logs. Instrumentation still works fine if we - # remove the reload. Not sure why this happens. - - handler_module = reload(handler_module) - - getattr(handler_module, handler_name)(event, MOCK_LAMBDA_CONTEXT) - - -class TestAwsLambdaInstrumentor(TestBase): - """AWS Lambda Instrumentation Testsuite""" - - @classmethod - def setUpClass(cls): - super().setUpClass() - sys.path.append(CONFIGURE_OTEL_SDK_SCRIPTS_DIR) - replace_in_file( - os.path.join(CONFIGURE_OTEL_SDK_SCRIPTS_DIR, "otel-instrument"), - 'LAMBDA_LAYER_PKGS_DIR = os.path.abspath(os.path.join(os.sep, "opt", "python"))', - f'LAMBDA_LAYER_PKGS_DIR = "{TOX_PYTHON_DIRECTORY}"', - ) - - def setUp(self): - super().setUp() - self.common_env_patch = mock.patch.dict( - "os.environ", - { - AWS_LAMBDA_EXEC_WRAPPER: "mock_aws_lambda_exec_wrapper", - "AWS_LAMBDA_FUNCTION_NAME": "test-python-lambda-function", - "AWS_LAMBDA_FUNCTION_VERSION": "2", - "AWS_REGION": "us-east-1", - _HANDLER: "mocks.lambda_function.handler", - "LAMBDA_RUNTIME_DIR": "mock-directory-since-tox-knows-pkgs-loc", - }, - ) - self.common_env_patch.start() - - # NOTE: Whether AwsLambdaInstrumentor().instrument() is run id decided - # by each test case. It depends on if the test is for auto or manual - # instrumentation. - - def tearDown(self): - super().tearDown() - self.common_env_patch.stop() - AwsLambdaInstrumentor().uninstrument() - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - sys.path.remove(CONFIGURE_OTEL_SDK_SCRIPTS_DIR) - replace_in_file( - os.path.join(CONFIGURE_OTEL_SDK_SCRIPTS_DIR, "otel-instrument"), - f'LAMBDA_LAYER_PKGS_DIR = "{TOX_PYTHON_DIRECTORY}"', - 'LAMBDA_LAYER_PKGS_DIR = os.path.abspath(os.path.join(os.sep, "opt", "python"))', - ) - - # MARK: Auto Instrumentation Tests - - def test_active_tracing(self): - test_env_patch = mock.patch.dict( - "os.environ", - { - **os.environ, - # Using Active tracing - _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_SAMPLED, - }, - ) - test_env_patch.start() - - mock_execute_lambda() - - spans = self.memory_exporter.get_finished_spans() - - assert spans - - self.assertEqual(len(spans), 1) - span = spans[0] - self.assertEqual(span.name, os.environ[ORIG_HANDLER]) - self.assertEqual(span.get_span_context().trace_id, MOCK_XRAY_TRACE_ID) - self.assertEqual(span.kind, SpanKind.SERVER) - self.assertSpanHasAttributes( - span, - { - ResourceAttributes.FAAS_ID: MOCK_LAMBDA_CONTEXT.invoked_function_arn, - SpanAttributes.FAAS_EXECUTION: MOCK_LAMBDA_CONTEXT.aws_request_id, - }, - ) - - # TODO: Waiting on OTel Python support for setting Resource Detectors - # using environment variables. Auto Instrumentation (used by this Lambda - # Instrumentation) sets up the global TracerProvider which is the only - # time Resource Detectors can be configured. - # - # environ["OTEL_RESOURCE_DETECTORS"] = "aws_lambda" - # - # We would configure this environment variable in - # `otel-instrument`. - # - # res_atts = span.resource.attributes - # self.assertEqual(res_atts[ResourceAttributes.CLOUD_PLATFORM], CloudPlatformValues.AWS_LAMBDA.value) - # self.assertEqual(res_atts[ResourceAttributes.CLOUD_PROVIDER], CloudProviderValues.AWS.value) - # self.assertEqual(res_atts[ResourceAttributes.CLOUD_REGION], os.environ["AWS_REGION"]) - # self.assertEqual(res_atts[ResourceAttributes.FAAS_NAME], os.environ["AWS_LAMBDA_FUNCTION_NAME"]) - # self.assertEqual(res_atts[ResourceAttributes.FAAS_VERSION], os.environ["AWS_LAMBDA_FUNCTION_VERSION"]) - - parent_context = span.parent - self.assertEqual( - parent_context.trace_id, span.get_span_context().trace_id - ) - self.assertEqual(parent_context.span_id, MOCK_XRAY_PARENT_SPAN_ID) - self.assertTrue(parent_context.is_remote) - - test_env_patch.stop() - - def test_parent_context_from_lambda_event(self): - test_env_patch = mock.patch.dict( - "os.environ", - { - **os.environ, - # NOT Active Tracing - _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, - # NOT using the X-Ray Propagator - OTEL_PROPAGATORS: "tracecontext", - }, - ) - test_env_patch.start() - - mock_execute_lambda( - { - "headers": { - TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED, - TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2", - } - } - ) - - spans = self.memory_exporter.get_finished_spans() - - assert spans - - self.assertEqual(len(spans), 1) - span = spans[0] - self.assertEqual(span.get_span_context().trace_id, MOCK_W3C_TRACE_ID) - - parent_context = span.parent - self.assertEqual( - parent_context.trace_id, span.get_span_context().trace_id - ) - self.assertEqual(parent_context.span_id, MOCK_W3C_PARENT_SPAN_ID) - self.assertEqual(len(parent_context.trace_state), 3) - self.assertEqual( - parent_context.trace_state.get(MOCK_W3C_TRACE_STATE_KEY), - MOCK_W3C_TRACE_STATE_VALUE, - ) - self.assertTrue(parent_context.is_remote) - - test_env_patch.stop() - - # MARK: Manual Instrumentation Tests - - def test_using_custom_extractor(self): - def custom_event_context_extractor(lambda_event): - return get_global_textmap().extract(lambda_event["foo"]["headers"]) - - test_env_patch = mock.patch.dict( - "os.environ", - { - **os.environ, - # DO NOT use `otel-instrument` script, resort to "manual" - # instrumentation as seen below - AWS_LAMBDA_EXEC_WRAPPER: "", - # NOT Active Tracing - _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, - # NOT using the X-Ray Propagator - OTEL_PROPAGATORS: "tracecontext", - }, - ) - test_env_patch.start() - - AwsLambdaInstrumentor().instrument( - event_context_extractor=custom_event_context_extractor, - ) - - mock_execute_lambda( - { - "foo": { - "headers": { - TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED, - TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2", - } - } - }, - should_reload=False, - ) - - spans = self.memory_exporter.get_finished_spans() - - assert spans - - self.assertEqual(len(spans), 1) - span = spans[0] - self.assertEqual(span.get_span_context().trace_id, MOCK_W3C_TRACE_ID) - - parent_context = span.parent - self.assertEqual( - parent_context.trace_id, span.get_span_context().trace_id - ) - self.assertEqual(parent_context.span_id, MOCK_W3C_PARENT_SPAN_ID) - self.assertEqual(len(parent_context.trace_state), 3) - self.assertEqual( - parent_context.trace_state.get(MOCK_W3C_TRACE_STATE_KEY), - MOCK_W3C_TRACE_STATE_VALUE, - ) - self.assertTrue(parent_context.is_remote) - - test_env_patch.stop() diff --git a/tox.ini b/tox.ini index 20f4d3eb6e..20dacc834c 100644 --- a/tox.ini +++ b/tox.ini @@ -25,9 +25,6 @@ envlist = py3{6,7,8,9}-test-instrumentation-aiopg ; instrumentation-aiopg intentionally excluded from pypy3 - ; opentelemetry-instrumentation-aws_lambda - py3{6,7,8,9}-test-instrumentation-aws_lambda - ; opentelemetry-instrumentation-botocore py3{6,7,8,9}-test-instrumentation-botocore pypy3-test-instrumentation-botocore @@ -216,7 +213,6 @@ changedir = test-instrumentation-aiopg: instrumentation/opentelemetry-instrumentation-aiopg/tests test-instrumentation-asgi: instrumentation/opentelemetry-instrumentation-asgi/tests test-instrumentation-asyncpg: instrumentation/opentelemetry-instrumentation-asyncpg/tests - test-instrumentation-aws_lambda: instrumentation/opentelemetry-instrumentation-aws_lambda/tests test-instrumentation-boto: instrumentation/opentelemetry-instrumentation-boto/tests test-instrumentation-botocore: instrumentation/opentelemetry-instrumentation-botocore/tests test-instrumentation-celery: instrumentation/opentelemetry-instrumentation-celery/tests @@ -278,8 +274,6 @@ commands_pre = asyncpg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg[test] - aws_lambda: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aws_lambda[test] - boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test] boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-boto[test] @@ -426,7 +420,6 @@ commands_pre = python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aws_lambda[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-datadog[test] python -m pip install -e {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-aws-xray[test]