diff --git a/.github/workflows/pr-python.yml b/.github/workflows/pr-python.yml index 9d079d3186..1992f6aea8 100644 --- a/.github/workflows/pr-python.yml +++ b/.github/workflows/pr-python.yml @@ -9,7 +9,7 @@ on: env: AWS_REGION: us-east-1 - CORE_REPO_SHA: e65baa4680cd9dafc026ca5ed760e6d2c2232c96 + CORE_REPO_SHA: 281c97bf8f9e31392859e006e13c9c8eac8967c3 jobs: PR-Python: diff --git a/python/README.md b/python/README.md index 3c3e994860..97162c6125 100644 --- a/python/README.md +++ b/python/README.md @@ -1,6 +1,6 @@ # OpenTelemetry Lambda Python -Layers for running Python applications on AWS Lambda with OpenTelemetry. +Scripts and files used to build AWS Lambda Layers for running OpenTelemetry on AWS Lambda for Python. ### Sample App diff --git a/python/src/function/lambda_function.py b/python/src/function/lambda_function.py deleted file mode 100644 index e29c112b38..0000000000 --- a/python/src/function/lambda_function.py +++ /dev/null @@ -1,29 +0,0 @@ -import os -import json -import aiohttp -import asyncio -import boto3 - - -async def fetch(session, url): - async with session.get(url) as response: - return await response.text() - - -async def callAioHttp(): - async with aiohttp.ClientSession() as session: - html = await fetch(session, "http://httpbin.org/") - - -s3 = boto3.resource("s3") - -# lambda function -def lambda_handler(event, context): - - loop = asyncio.get_event_loop() - loop.run_until_complete(callAioHttp()) - - for bucket in s3.buckets.all(): - print(bucket.name) - - return {"statusCode": 200, "body": json.dumps(os.environ.get("_X_AMZN_TRACE_ID"))} diff --git a/python/src/function/requirements.txt b/python/src/function/requirements.txt deleted file mode 100644 index ce2357185a..0000000000 --- a/python/src/function/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -aiohttp \ No newline at end of file diff --git a/python/src/otel/Dockerfile b/python/src/otel/Dockerfile index 49879281ae..4839484149 100644 --- a/python/src/otel/Dockerfile +++ b/python/src/otel/Dockerfile @@ -8,15 +8,12 @@ WORKDIR /workspace RUN mkdir -p /build && \ python3 -m pip install -r otel_sdk/requirements.txt -t /build/python && \ - python3 -m pip install -r otel_sdk/requirements-nodeps.txt -t /build/tmp --no-deps && \ - cp -r /build/tmp/* /build/python/ && \ - rm -rf /build/tmp && \ - cp -r otel_sdk/* /build/python && \ - mv /build/python/otel-instrument /build/otel-instrument && \ + python3 -m pip install -r otel_sdk/requirements-nodeps.txt -t /build/python --no-deps && \ + cp -r otel_sdk/otel_wrapper.py /build/python && \ + cp -r otel_sdk/otel-instrument /build && \ chmod 755 /build/otel-instrument && \ rm -rf /build/python/boto* && \ rm -rf /build/python/urllib3* && \ - cd /build && \ - zip -r layer.zip otel-instrument python + zip -r /build/layer.zip /build CMD cp /build/layer.zip /out/layer.zip diff --git a/python/src/otel/otel_sdk/opentelemetry/instrumentation/aws_lambda/__init__.py b/python/src/otel/otel_sdk/opentelemetry/instrumentation/aws_lambda/__init__.py deleted file mode 100644 index 27d245b32f..0000000000 --- a/python/src/otel/otel_sdk/opentelemetry/instrumentation/aws_lambda/__init__.py +++ /dev/null @@ -1,234 +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. - -# TODO: usage -""" -The opentelemetry-instrumentation-aws-lambda package allows tracing AWS -Lambda function. - -Usage ------ - -.. code:: python - # Copy this snippet into AWS Lambda function - # Ref Doc: https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html - - import boto3 - from opentelemetry.instrumentation.aws_lambda import ( - AwsLambdaInstrumentor - ) - - # Enable instrumentation - AwsLambdaInstrumentor().instrument(skip_dep_check=True) - - # Lambda function - def lambda_handler(event, context): - s3 = boto3.resource('s3') - for bucket in s3.buckets.all(): - print(bucket.name) - - return "200 OK" - -API ---- -""" - -import logging -import os -from importlib import import_module -from typing import Any, Collection - -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.trace import SpanAttributes -from opentelemetry.trace import ( - SpanKind, - Tracer, - get_tracer, - get_tracer_provider, -) -from opentelemetry.trace.propagation import get_current_span -from wrapt import wrap_function_wrapper - -logger = logging.getLogger(__name__) - - -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. - """ - tracer = get_tracer( - __name__, __version__, kwargs.get("tracer_provider") - ) - event_context_extractor = kwargs.get("event_context_extractor") - - lambda_handler = os.environ.get( - "ORIG_HANDLER", os.environ.get("_HANDLER") - ) - wrapped_names = lambda_handler.rsplit(".", 1) - self._wrapped_module_name = wrapped_names[0] - self._wrapped_function_name = wrapped_names[1] - - flush_timeout = os.environ.get("OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT", 30000) - - _instrument( - tracer, - self._wrapped_module_name, - self._wrapped_function_name, - flush_timeout, - 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 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( - tracer: Tracer, - wrapped_module_name, - wrapped_function_name, - flush_timeout, - 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) - - 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] - # Refer: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/faas.md#example - span.set_attribute( - SpanAttributes.FAAS_EXECUTION, lambda_context.aws_request_id - ) - span.set_attribute( - "faas.id", lambda_context.invoked_function_arn - ) - - # TODO: fix in Collector because they belong resource attrubutes - span.set_attribute( - "faas.name", os.environ.get("AWS_LAMBDA_FUNCTION_NAME") - ) - span.set_attribute( - "faas.version", - os.environ.get("AWS_LAMBDA_FUNCTION_VERSION"), - ) - - result = call_wrapped(*args, **kwargs) - - # force_flush before function quit in case of Lambda freeze. - tracer_provider = get_tracer_provider() - tracer_provider.force_flush(flush_timeout) - - return result - - wrap_function_wrapper( - wrapped_module_name, - wrapped_function_name, - _instrumented_lambda_handler_call, - ) diff --git a/python/src/otel/otel_sdk/opentelemetry/instrumentation/aws_lambda/version.py b/python/src/otel/otel_sdk/opentelemetry/instrumentation/aws_lambda/version.py deleted file mode 100644 index 3de97689b3..0000000000 --- a/python/src/otel/otel_sdk/opentelemetry/instrumentation/aws_lambda/version.py +++ /dev/null @@ -1,15 +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. - -__version__ = "0.25b2" diff --git a/python/src/otel/otel_sdk/requirements-nodeps.txt b/python/src/otel/otel_sdk/requirements-nodeps.txt index 85676087fc..beb30efeab 100644 --- a/python/src/otel/otel_sdk/requirements-nodeps.txt +++ b/python/src/otel/otel_sdk/requirements-nodeps.txt @@ -1,29 +1,30 @@ -opentelemetry-instrumentation-aiohttp-client==0.25b2 -opentelemetry-util-http==0.25b2 +opentelemetry-instrumentation-aiohttp-client==0.26b1 +opentelemetry-util-http==0.26b1 asgiref~=3.0 -opentelemetry-instrumentation-asgi==0.25b2 -opentelemetry-instrumentation-asyncpg==0.25b2 -opentelemetry-instrumentation-boto==0.25b2 -opentelemetry-instrumentation-botocore==0.25b2 -opentelemetry-instrumentation-celery==0.25b2 -opentelemetry-instrumentation-dbapi==0.25b2 -opentelemetry-instrumentation-django==0.25b2 -opentelemetry-instrumentation-elasticsearch==0.25b2 -opentelemetry-instrumentation-fastapi==0.25b2 -opentelemetry-instrumentation-falcon==0.25b2 -opentelemetry-instrumentation-flask==0.25b2 -opentelemetry-instrumentation-grpc==0.25b2 -opentelemetry-instrumentation-jinja2==0.25b2 -opentelemetry-instrumentation-mysql==0.25b2 -opentelemetry-instrumentation-psycopg2==0.25b2 -opentelemetry-instrumentation-pymemcache==0.25b2 -opentelemetry-instrumentation-pymongo==0.25b2 -opentelemetry-instrumentation-pymysql==0.25b2 -opentelemetry-instrumentation-pyramid==0.25b2 -opentelemetry-instrumentation-redis==0.25b2 -opentelemetry-instrumentation-requests==0.25b2 -opentelemetry-instrumentation-sqlalchemy==0.25b2 -opentelemetry-instrumentation-sqlite3==0.25b2 -opentelemetry-instrumentation-starlette==0.25b2 -opentelemetry-instrumentation-tornado==0.25b2 -opentelemetry-instrumentation-wsgi==0.25b2 \ No newline at end of file +opentelemetry-instrumentation-asgi==0.26b1 +opentelemetry-instrumentation-aws-lambda==0.26b1 +opentelemetry-instrumentation-asyncpg==0.26b1 +opentelemetry-instrumentation-boto==0.26b1 +opentelemetry-instrumentation-botocore==0.26b1 +opentelemetry-instrumentation-celery==0.26b1 +opentelemetry-instrumentation-dbapi==0.26b1 +opentelemetry-instrumentation-django==0.26b1 +opentelemetry-instrumentation-elasticsearch==0.26b1 +opentelemetry-instrumentation-fastapi==0.26b1 +opentelemetry-instrumentation-falcon==0.26b1 +opentelemetry-instrumentation-flask==0.26b1 +opentelemetry-instrumentation-grpc==0.26b1 +opentelemetry-instrumentation-jinja2==0.26b1 +opentelemetry-instrumentation-mysql==0.26b1 +opentelemetry-instrumentation-psycopg2==0.26b1 +opentelemetry-instrumentation-pymemcache==0.26b1 +opentelemetry-instrumentation-pymongo==0.26b1 +opentelemetry-instrumentation-pymysql==0.26b1 +opentelemetry-instrumentation-pyramid==0.26b1 +opentelemetry-instrumentation-redis==0.26b1 +opentelemetry-instrumentation-requests==0.26b1 +opentelemetry-instrumentation-sqlalchemy==0.26b1 +opentelemetry-instrumentation-sqlite3==0.26b1 +opentelemetry-instrumentation-starlette==0.26b1 +opentelemetry-instrumentation-tornado==0.26b1 +opentelemetry-instrumentation-wsgi==0.26b1 \ No newline at end of file diff --git a/python/src/otel/otel_sdk/requirements.txt b/python/src/otel/otel_sdk/requirements.txt index 2bc0eaebbb..81f73f7e15 100644 --- a/python/src/otel/otel_sdk/requirements.txt +++ b/python/src/otel/otel_sdk/requirements.txt @@ -1,4 +1,4 @@ -opentelemetry-exporter-otlp==1.6.2 -opentelemetry-distro==0.25b2 +opentelemetry-exporter-otlp==1.7.1 +opentelemetry-distro==0.26b1 opentelemetry-propagator-aws-xray==1.0.1 -opentelemetry-instrumentation==0.25b2 \ No newline at end of file +opentelemetry-instrumentation==0.26b1 \ No newline at end of file diff --git a/python/src/otel/setup.cfg b/python/src/otel/setup.cfg deleted file mode 100644 index a3fcc1e354..0000000000 --- a/python/src/otel/setup.cfg +++ /dev/null @@ -1,56 +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. -# -[metadata] -name = opentelemetry-instrumentation-aws-lambda -description = OpenTelemetry AWS Lambda instrumentation -long_description = file: README.rst -long_description_content_type = text/x-rst -author = OpenTelemetry Authors -author_email = cncf-opentelemetry-contributors@lists.cncf.io -url = https://github.com/open-telemetry/opentelemetry-lambda/tree/main/python/src/otel -platforms = any -license = Apache-2.0 -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - -[options] -python_requires = >=3.6 -package_dir= - =otel_sdk -packages=find_namespace: -install_requires = - opentelemetry-distro == 0.25b2 - opentelemetry-exporter-otlp == 1.6.2 - opentelemetry-propagator-aws-xray == 1.0.1 - -[options.extras_require] -test = - opentelemetry-test == 0.25b2 - -[options.packages.find] -where = otel_sdk - -[options.entry_points] -# NOTE: (NathanielRN) DO NOT add AwsLambdaInstrumentor entry point because -# current AWS Lambda implementation reloads a fresh import of the user's Lambda -# handler. Auto Instrumentation runs _before_ and if it instruments the handler -# that patching will be lost. diff --git a/python/src/otel/setup.py b/python/src/otel/setup.py deleted file mode 100644 index bc239b2981..0000000000 --- a/python/src/otel/setup.py +++ /dev/null @@ -1,99 +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. - - -# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM templates/instrumentation_setup.py.txt. -# RUN `python scripts/generate_setup.py` TO REGENERATE. - - -import distutils.cmd -import json -import os -from configparser import ConfigParser - -import setuptools - -config = ConfigParser() -config.read("setup.cfg") - -# We provide extras_require parameter to setuptools.setup later which -# overwrites the extra_require section from setup.cfg. To support extra_require -# secion in setup.cfg, we load it here and merge it with the extra_require param. -extras_require = {} -if "options.extras_require" in config: - for key, value in config["options.extras_require"].items(): - extras_require[key] = [v for v in value.split("\n") if v.strip()] - -BASE_DIR = os.path.dirname(__file__) -PACKAGE_INFO = {} - -VERSION_FILENAME = os.path.join( - BASE_DIR, - "otel_sdk", - "opentelemetry", - "instrumentation", - "aws_lambda", - "version.py", -) -with open(VERSION_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -PACKAGE_FILENAME = os.path.join( - BASE_DIR, - "otel_sdk", - "opentelemetry", - "instrumentation", - "aws_lambda", - "package.py", -) -with open(PACKAGE_FILENAME, encoding="utf-8") as f: - exec(f.read(), PACKAGE_INFO) - -# Mark any instruments/runtime dependencies as test dependencies as well. -extras_require["instruments"] = PACKAGE_INFO["_instruments"] -test_deps = extras_require.get("test", []) -for dep in extras_require["instruments"]: - test_deps.append(dep) - -extras_require["test"] = test_deps - - -class JSONMetadataCommand(distutils.cmd.Command): - - description = ( - "print out package metadata as JSON. This is used by OpenTelemetry dev scripts to ", - "auto-generate code in other places", - ) - user_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - metadata = { - "name": config["metadata"]["name"], - "version": PACKAGE_INFO["__version__"], - "instruments": PACKAGE_INFO["_instruments"], - } - print(json.dumps(metadata)) - - -setuptools.setup( - cmdclass={"meta": JSONMetadataCommand}, - version=PACKAGE_INFO["__version__"], - extras_require=extras_require, -) diff --git a/python/src/otel/tests/mock_user_lambda.py b/python/src/otel/tests/mock_user_lambda.py deleted file mode 100644 index 2761db8f06..0000000000 --- a/python/src/otel/tests/mock_user_lambda.py +++ /dev/null @@ -1,2 +0,0 @@ -def handler(event, context): - return "200 ok" diff --git a/python/src/otel/otel_sdk/opentelemetry/instrumentation/aws_lambda/package.py b/python/src/otel/tests/mocks/lambda_function.py similarity index 92% rename from python/src/otel/otel_sdk/opentelemetry/instrumentation/aws_lambda/package.py rename to python/src/otel/tests/mocks/lambda_function.py index 211727b883..c292575651 100644 --- a/python/src/otel/otel_sdk/opentelemetry/instrumentation/aws_lambda/package.py +++ b/python/src/otel/tests/mocks/lambda_function.py @@ -13,4 +13,5 @@ # limitations under the License. -_instruments = () +def handler(event, context): + return "200 ok" diff --git a/python/src/otel/tests/test_otel.py b/python/src/otel/tests/test_otel.py index f09f9854b6..917ebe0ae6 100644 --- a/python/src/otel/tests/test_otel.py +++ b/python/src/otel/tests/test_otel.py @@ -11,6 +11,12 @@ # 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. + +""" +This file tests that the `otel-instrument` script included in this repository +successfully instruments OTel Python in a mock Lambda environment. +""" + import fileinput import os import subprocess @@ -19,8 +25,13 @@ from shutil import which from unittest import mock -from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor -from opentelemetry.propagate import get_global_textmap +from opentelemetry.environment_variables import OTEL_PROPAGATORS +from opentelemetry.instrumentation.aws_lambda import ( + _HANDLER, + _X_AMZN_TRACE_ID, + ORIG_HANDLER, + AwsLambdaInstrumentor, +) from opentelemetry.propagators.aws.aws_xray_propagator import ( TRACE_ID_FIRST_PART_LENGTH, TRACE_ID_VERSION, @@ -29,14 +40,14 @@ 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, +) -_HANDLER = "_HANDLER" -_X_AMZN_TRACE_ID = "_X_AMZN_TRACE_ID" AWS_LAMBDA_EXEC_WRAPPER = "AWS_LAMBDA_EXEC_WRAPPER" -INSTRUMENTATION_SRC_DIR = os.path.join( +INIT_OTEL_SCRIPTS_DIR = os.path.join( *(os.path.dirname(__file__), "..", "otel_sdk") ) -ORIG_HANDLER = "ORIG_HANDLER" TOX_PYTHON_DIRECTORY = os.path.dirname(os.path.dirname(which("python3"))) @@ -60,8 +71,9 @@ def __init__(self, aws_request_id, invoked_function_arn): f"{MOCK_XRAY_TRACE_CONTEXT_COMMON};Sampled=0" ) -# Read more: +# 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 = ( @@ -98,14 +110,14 @@ def mock_aws_lambda_exec_wrapper(): # call other instrumented libraries so we have no use for it for now. print_environ_program = ( - "from os import environ;" - "print(f\"ORIG_HANDLER={environ['ORIG_HANDLER']}\");" - "print(f\"_HANDLER={environ['_HANDLER']}\");" + "import os;" + f"print(f\"{ORIG_HANDLER}={{os.environ['{ORIG_HANDLER}']}}\");" + f"print(f\"{_HANDLER}={{os.environ['{_HANDLER}']}}\");" ) completed_subprocess = subprocess.run( [ - os.path.join(INSTRUMENTATION_SRC_DIR, "otel-instrument"), + os.path.join(INIT_OTEL_SCRIPTS_DIR, "otel-instrument"), "python3", "-c", print_environ_program, @@ -126,18 +138,28 @@ def mock_aws_lambda_exec_wrapper(): def mock_execute_lambda(event=None): - """Mocks Lambda importing and then calling the method at the current - `_HANDLER` environment variable. Like the real Lambda, if - `AWS_LAMBDA_EXEC_WRAPPER` is defined, if executes that before `_HANDLER`. + """Mocks the AWS Lambda execution. Mocks importing and then calling the + method at the current `_HANDLER` environment variable. Like the real Lambda, + if `AWS_LAMBDA_EXEC_WRAPPER` is defined, it executes that before `_HANDLER`. + + NOTE: We don't use `moto`'s `mock_lambda` because we are not instrumenting + calls to AWS Lambda using the AWS SDK. Instead, we are instrumenting AWS + Lambda itself. See more: - https://aws-otel.github.io/docs/getting-started/lambda/lambda-python + 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. """ - if os.environ[AWS_LAMBDA_EXEC_WRAPPER]: - globals()[os.environ[AWS_LAMBDA_EXEC_WRAPPER]]() - module_name, handler_name = os.environ[_HANDLER].split(".") - handler_module = import_module(".".join(module_name.split("/"))) + # The point of the repo is to test using the script, so we can count on it + # being here for every test and do not check for its existence. + # if os.environ[AWS_LAMBDA_EXEC_WRAPPER]: + globals()[os.environ[AWS_LAMBDA_EXEC_WRAPPER]]() + + module_name, handler_name = os.environ[_HANDLER].rsplit(".", 1) + handler_module = import_module(module_name.replace("/", ".")) getattr(handler_module, handler_name)(event, MOCK_LAMBDA_CONTEXT) @@ -147,9 +169,9 @@ class TestAwsLambdaInstrumentor(TestBase): @classmethod def setUpClass(cls): super().setUpClass() - sys.path.append(INSTRUMENTATION_SRC_DIR) + sys.path.append(INIT_OTEL_SCRIPTS_DIR) replace_in_file( - os.path.join(INSTRUMENTATION_SRC_DIR, "otel-instrument"), + os.path.join(INIT_OTEL_SCRIPTS_DIR, "otel-instrument"), 'export LAMBDA_LAYER_PKGS_DIR="/opt/python"', f'export LAMBDA_LAYER_PKGS_DIR="{TOX_PYTHON_DIRECTORY}"', ) @@ -160,10 +182,7 @@ def setUp(self): "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: "mock_user_lambda.handler", + _HANDLER: "mocks.lambda_function.handler", }, ) self.common_env_patch.start() @@ -176,9 +195,9 @@ def tearDown(self): @classmethod def tearDownClass(cls): super().tearDownClass() - sys.path.remove(INSTRUMENTATION_SRC_DIR) + sys.path.remove(INIT_OTEL_SCRIPTS_DIR) replace_in_file( - os.path.join(INSTRUMENTATION_SRC_DIR, "otel-instrument"), + os.path.join(INIT_OTEL_SCRIPTS_DIR, "otel-instrument"), f'export LAMBDA_LAYER_PKGS_DIR="{TOX_PYTHON_DIRECTORY}"', 'export LAMBDA_LAYER_PKGS_DIR="/opt/python"', ) @@ -188,6 +207,7 @@ def test_active_tracing(self): "os.environ", { **os.environ, + # Using Active tracing _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_SAMPLED, }, ) @@ -201,7 +221,7 @@ def test_active_tracing(self): self.assertEqual(len(spans), 1) span = spans[0] - self.assertEqual(span.name, os.environ["ORIG_HANDLER"]) + 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( @@ -241,7 +261,7 @@ def test_parent_context_from_lambda_event(self): # NOT Active Tracing _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, # NOT using the X-Ray Propagator - "OTEL_PROPAGATORS": "tracecontext", + OTEL_PROPAGATORS: "tracecontext", }, ) test_env_patch.start() @@ -249,69 +269,8 @@ def test_parent_context_from_lambda_event(self): mock_execute_lambda( { "headers": { - "traceparent": MOCK_W3C_TRACE_CONTEXT_SAMPLED, - "tracestate": 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() - - 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 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() - - # NOTE: Instead of using `AWS_LAMBDA_EXEC_WRAPPER` to point `_HANDLER` - # to a module which instruments and calls the user `ORIG_HANDLER`, we - # leave `_HANDLER` as is and replace `AWS_LAMBDA_EXEC_WRAPPER` with this - # line below. This is like "manual" instrumentation for Lambda. - AwsLambdaInstrumentor().instrument( - event_context_extractor=custom_event_context_extractor, - skip_dep_check=True, - ) - - mock_execute_lambda( - { - "foo": { - "headers": { - "traceparent": MOCK_W3C_TRACE_CONTEXT_SAMPLED, - "tracestate": f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2", - } + 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", } } ) diff --git a/python/src/tox.ini b/python/src/tox.ini index 7240fb91d1..0828cddf3b 100644 --- a/python/src/tox.ini +++ b/python/src/tox.ini @@ -23,11 +23,11 @@ changedir = test-instrumentation-aws-lambda: {toxinidir}/otel/tests commands_pre = - test: pip install "opentelemetry-test[test] @ {env:CORE_REPO}#egg=opentelemetry-test&subdirectory=tests/util" + test: pip install "opentelemetry-test-utils @ {env:CORE_REPO}#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils" - aws-lambda: pip install {toxinidir}/otel[test] + aws-lambda: pip install opentelemetry-instrumentation-aws-lambda -deps = +deps = test: pytest commands =