From c81995d0ce89d39104cac9914bda95bf642602cb Mon Sep 17 00:00:00 2001 From: Nathaniel Ruiz Nowell Date: Thu, 8 Oct 2020 21:12:59 -0700 Subject: [PATCH 1/6] Add SDK Extension for Compatibility with AWS X-Ray --- .github/workflows/test.yml | 2 +- .../CHANGELOG.md | 6 + .../opentelemetry-sdk-extension-aws/LICENSE | 201 +++++++++++ .../MANIFEST.rst | 9 + .../README.rst | 51 +++ .../opentelemetry-sdk-extension-aws/setup.cfg | 53 +++ .../opentelemetry-sdk-extension-aws/setup.py | 26 ++ .../sdk/extension/aws/trace/__init__.py | 19 + .../aws/trace/aws_xray_ids_generator.py | 37 ++ .../aws/trace/propagation/__init__.py | 0 .../aws/trace/propagation/aws_xray_format.py | 285 +++++++++++++++ .../sdk/extension/aws/version.py | 15 + .../tests/__init__.py | 13 + .../tests/trace/__init__.py | 13 + .../tests/trace/propagation/__init__.py | 0 .../trace/propagation/test_aws_xray_format.py | 340 ++++++++++++++++++ .../trace/test_aws_xray_ids_generator.py | 43 +++ tox.ini | 8 + 18 files changed, 1120 insertions(+), 1 deletion(-) create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/CHANGELOG.md create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/LICENSE create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/MANIFEST.rst create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/README.rst create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/setup.cfg create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/setup.py create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/__init__.py create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/aws_xray_ids_generator.py create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/__init__.py create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/version.py create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/tests/__init__.py create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/__init__.py create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/__init__.py create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py create mode 100644 sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/test_aws_xray_ids_generator.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5e2dead6dc..fc2c043d17 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: python-version: [ py35, py36, py37, py38, pypy3 ] - package: ["instrumentation", "exporter"] + package: ["instrumentation", "exporter", "sdkextension"] os: [ ubuntu-latest ] include: # py35-instrumentation segfaults on 18.04 so we instead run on 20.04 diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/CHANGELOG.md b/sdk-extension/opentelemetry-sdk-extension-aws/CHANGELOG.md new file mode 100644 index 0000000000..7a82dad1c9 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +## Unreleased + +- Provide components needed to Configure OTel SDK for Tracing with AWS X-Ray + ([#130](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/130)) diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/LICENSE b/sdk-extension/opentelemetry-sdk-extension-aws/LICENSE new file mode 100644 index 0000000000..f49a4e16e6 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. \ No newline at end of file diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/MANIFEST.rst b/sdk-extension/opentelemetry-sdk-extension-aws/MANIFEST.rst new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/MANIFEST.rst @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/README.rst b/sdk-extension/opentelemetry-sdk-extension-aws/README.rst new file mode 100644 index 0000000000..806193ab4a --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/README.rst @@ -0,0 +1,51 @@ +OpenTelemetry SDK Extension for AWS X-Ray Compatibility +======================================================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-sdk-extension-aws.svg + :target: https://pypi.org/project/opentelemetry-sdk-extension-aws/ + + +This library provides components necessary to configure the OpenTelemetry SDK +for tracing with AWS X-Ray. + +Installation +------------ + +:: + + pip install opentelemetry-sdk-extension-aws + + +Usage (AWS X-Ray IDs Generator) +------------------------------- + +Configure the OTel SDK TracerProvider with the provided custom IDs Generator to +make spans compatible with the AWS X-Ray backend tracing service. + +.. code-block:: python + + from opentelemetry.sdk.extension.aws.trace import AwsXRayIdsGenerator + + trace.set_tracer_provider( + TracerProvider(ids_generator=AwsXRayIdsGenerator()) + ) + + +Usage (AWS X-Ray Propagator) +---------------------------- + +Set this environment variable to have the OTel SDK use the provided AWS X-Ray +Propagator: + +:: + + export OTEL_PYTHON_PROPAGATORS = aws_xray + + +References +---------- + +* `OpenTelemetry Project `_ +* `AWS X-Ray Trace IDs Format `_ diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/setup.cfg b/sdk-extension/opentelemetry-sdk-extension-aws/setup.cfg new file mode 100644 index 0000000000..4aa890ca53 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/setup.cfg @@ -0,0 +1,53 @@ +# 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-sdk-extension-aws +description = AWS SDK extension for OpenTelemetry +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-python-contrib/tree/master/sdk-extension/opentelemetry-sdk-extension-aws +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.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.15.b0 + +[options.entry_points] +opentelemetry_propagator = + aws_xray = opentelemetry.sdk.extension.aws.trace.propagation.aws_xray_format:AwsXRayFormat + +[options.extras_require] +test = + opentelemetry-test == 0.15.b0 + +[options.packages.find] +where = src diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/setup.py b/sdk-extension/opentelemetry-sdk-extension-aws/setup.py new file mode 100644 index 0000000000..a5b0803743 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/setup.py @@ -0,0 +1,26 @@ +# 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 os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "sdk", "extension", "aws", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/__init__.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/__init__.py new file mode 100644 index 0000000000..f33a095c01 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/__init__.py @@ -0,0 +1,19 @@ +# 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. + +from opentelemetry.sdk.extension.aws.trace.aws_xray_ids_generator import ( + AwsXRayIdsGenerator, +) + +__all__ = ["AwsXRayIdsGenerator"] diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/aws_xray_ids_generator.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/aws_xray_ids_generator.py new file mode 100644 index 0000000000..799c16032a --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/aws_xray_ids_generator.py @@ -0,0 +1,37 @@ +# 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 datetime +import random + +from opentelemetry import trace + + +class AwsXRayIdsGenerator(trace.IdsGenerator): + """Generates tracing IDs compatible with the AWS X-Ray tracing service. In + the X-Ray system, the first 32 bits of the `TraceId` are the Unix epoch time + in seconds. Since spans (AWS calls them segments) with an embedded timestamp + more than 30 days ago are rejected, a purely random `TraceId` risks being + rejected by the service. + + See: https://docs.aws.amazon.com/xray/latest/devguide/xray-api-sendingdata.html#xray-api-traceids + """ + + def generate_span_id(self) -> int: + return trace.RandomIdsGenerator().generate_span_id() + + def generate_trace_id(self) -> int: + trace_time = int(datetime.datetime.utcnow().timestamp()) + trace_identifier = random.getrandbits(96) + return (trace_time << 96) + trace_identifier diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/__init__.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py new file mode 100644 index 0000000000..17ef67ed9d --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py @@ -0,0 +1,285 @@ +# 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 logging +import typing + +import opentelemetry.trace as trace +from opentelemetry.context import Context +from opentelemetry.trace.propagation.textmap import ( + Getter, + Setter, + TextMapPropagator, + TextMapPropagatorT, +) + +_logger = logging.getLogger(__name__) + + +class AwsXRayFormat(TextMapPropagator): + """Propagator for the AWS X-Ray Trace Header propagation protocol. + + See: https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader + """ + + # AWS + TRACE_HEADER_KEY = "X-Amzn-Trace-Id" + + KV_PAIR_DELIMITER = ";" + KEY_AND_VALUE_DELIMITER = "=" + + TRACE_ID_KEY = "Root" + TRACE_ID_LENGTH = 35 + TRACE_ID_VERSION = "1" + TRACE_ID_DELIMITER = "-" + TRACE_ID_DELIMITER_INDEX_1 = 1 + TRACE_ID_DELIMITER_INDEX_2 = 10 + TRACE_ID_FIRST_PART_LENGTH = 8 + + PARENT_ID_KEY = "Parent" + PARENT_ID_LENGTH = 16 + + SAMPLED_FLAG_KEY = "Sampled" + SAMPLED_FLAG_LENGTH = 1 + IS_SAMPLED = "1" + NOT_SAMPLED = "0" + + # pylint: disable=too-many-locals + # pylint: disable=too-many-return-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + def extract( + self, + getter: Getter[TextMapPropagatorT], + carrier: TextMapPropagatorT, + context: typing.Optional[Context] = None, + ) -> Context: + trace_header_list = getter(carrier, self.TRACE_HEADER_KEY) + trace_header_list = getter.get(carrier, self.TRACE_HEADER_KEY) + + if not trace_header_list or len(trace_header_list) != 1: + return trace.set_span_in_context( + trace.INVALID_SPAN, context=context + ) + + trace_header = trace_header_list[0] + + if not trace_header: + return trace.set_span_in_context( + trace.INVALID_SPAN, context=context + ) + + trace_id = trace.INVALID_TRACE_ID + span_id = trace.INVALID_SPAN_ID + sampled = False + + next_kv_pair_start = 0 + + while next_kv_pair_start < len(trace_header): + try: + kv_pair_delimiter_index = trace_header.index( + self.KV_PAIR_DELIMITER, next_kv_pair_start + ) + kv_pair_subset = trace_header[ + next_kv_pair_start:kv_pair_delimiter_index + ] + next_kv_pair_start = kv_pair_delimiter_index + 1 + except ValueError: + kv_pair_subset = trace_header[next_kv_pair_start:] + next_kv_pair_start = len(trace_header) + + stripped_kv_pair = kv_pair_subset.strip() + + try: + key_and_value_delimiter_index = stripped_kv_pair.index( + self.KEY_AND_VALUE_DELIMITER + ) + except ValueError: + _logger.error( + ( + "Error parsing X-Ray trace header. Invalid key value pair: %s. Returning INVALID span context.", + kv_pair_subset, + ) + ) + return trace.set_span_in_context( + trace.INVALID_SPAN, context=context + ) + + value = stripped_kv_pair[key_and_value_delimiter_index + 1 :] + + if stripped_kv_pair.startswith(self.TRACE_ID_KEY): + if ( + len(value) != self.TRACE_ID_LENGTH + or not value.startswith(self.TRACE_ID_VERSION) + or value[self.TRACE_ID_DELIMITER_INDEX_1] + != self.TRACE_ID_DELIMITER + or value[self.TRACE_ID_DELIMITER_INDEX_2] + != self.TRACE_ID_DELIMITER + ): + _logger.error( + ( + "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", + self.TRACE_HEADER_KEY, + trace_header, + ) + ) + return trace.set_span_in_context( + trace.INVALID_SPAN, context=context + ) + + timestamp_subset = value[ + self.TRACE_ID_DELIMITER_INDEX_1 + + 1 : self.TRACE_ID_DELIMITER_INDEX_2 + ] + unique_id_subset = value[ + self.TRACE_ID_DELIMITER_INDEX_2 + 1 : self.TRACE_ID_LENGTH + ] + try: + trace_id = int(timestamp_subset + unique_id_subset, 16) + except ValueError: + _logger.error( + ( + "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", + self.TRACE_HEADER_KEY, + trace_header, + ) + ) + return trace.set_span_in_context( + trace.INVALID_SPAN, context=context + ) + elif stripped_kv_pair.startswith(self.PARENT_ID_KEY): + if len(value) != self.PARENT_ID_LENGTH: + _logger.error( + ( + "Invalid ParentId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", + self.TRACE_HEADER_KEY, + trace_header, + ) + ) + return trace.set_span_in_context( + trace.INVALID_SPAN, context=context + ) + + try: + span_id = int(value, 16) + except ValueError: + _logger.error( + ( + "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", + self.TRACE_HEADER_KEY, + trace_header, + ) + ) + return trace.set_span_in_context( + trace.INVALID_SPAN, context=context + ) + elif stripped_kv_pair.startswith(self.SAMPLED_FLAG_KEY): + is_sampled_flag_valid = True + + if len(value) != self.SAMPLED_FLAG_LENGTH: + is_sampled_flag_valid = False + + if is_sampled_flag_valid: + sampled_flag = value[0] + if sampled_flag == self.IS_SAMPLED: + sampled = True + elif sampled_flag == self.NOT_SAMPLED: + sampled = False + else: + is_sampled_flag_valid = False + + if not is_sampled_flag_valid: + _logger.error( + ( + "Invalid Sampling flag in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", + self.TRACE_HEADER_KEY, + trace_header, + ) + ) + return trace.set_span_in_context( + trace.INVALID_SPAN, context=context + ) + + options = 0 + if sampled: + options |= trace.TraceFlags.SAMPLED + + span_context = trace.SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=True, + trace_flags=trace.TraceFlags(options), + trace_state=trace.TraceState(), + ) + + if not span_context.is_valid: + _logger.error( + "Invalid Span Extracted. Insertting INVALID span into provided context." + ) + return trace.set_span_in_context( + trace.INVALID_SPAN, context=context + ) + + return trace.set_span_in_context( + trace.DefaultSpan(span_context), context=context + ) + + def inject( + self, + set_in_carrier: Setter[TextMapPropagatorT], + carrier: TextMapPropagatorT, + context: typing.Optional[Context] = None, + ) -> None: + span = trace.get_current_span(context=context) + + span_context = span.get_span_context() + if not span_context.is_valid: + return + + otel_trace_id = "{:032x}".format(span_context.trace_id) + xray_trace_id = ( + self.TRACE_ID_VERSION + + self.TRACE_ID_DELIMITER + + otel_trace_id[: self.TRACE_ID_FIRST_PART_LENGTH] + + self.TRACE_ID_DELIMITER + + otel_trace_id[self.TRACE_ID_FIRST_PART_LENGTH :] + ) + + parent_id = "{:016x}".format(span_context.span_id) + + sampling_flag = ( + self.IS_SAMPLED + if span_context.trace_flags & trace.TraceFlags.SAMPLED + else self.NOT_SAMPLED + ) + + # TODO: Add OT trace state to the X-Ray trace header + + trace_header = ( + self.TRACE_ID_KEY + + self.KEY_AND_VALUE_DELIMITER + + xray_trace_id + + self.KV_PAIR_DELIMITER + + self.PARENT_ID_KEY + + self.KEY_AND_VALUE_DELIMITER + + parent_id + + self.KV_PAIR_DELIMITER + + self.SAMPLED_FLAG_KEY + + self.KEY_AND_VALUE_DELIMITER + + sampling_flag + ) + + set_in_carrier( + carrier, self.TRACE_HEADER_KEY, trace_header, + ) diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/version.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/version.py new file mode 100644 index 0000000000..1f98d44fa8 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/version.py @@ -0,0 +1,15 @@ +# 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. + +__version__ = "0.16.dev0" diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/__init__.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/__init__.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/__init__.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py new file mode 100644 index 0000000000..fc0946baba --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py @@ -0,0 +1,340 @@ +# 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 unittest + +from requests.structures import CaseInsensitiveDict + +import opentelemetry.trace as trace_api +from opentelemetry.sdk.extension.aws.trace.propagation.aws_xray_format import ( + AwsXRayFormat, +) +from opentelemetry.trace import ( + DEFAULT_TRACE_OPTIONS, + DEFAULT_TRACE_STATE, + INVALID_SPAN_CONTEXT, + SpanContext, + TraceFlags, + TraceState, + set_span_in_context, +) + +TRACE_ID_BASE16 = "8a3c60f7d188f8fa79d48a391a778fa6" + +SPAN_ID_BASE16 = "53995c3f42cd8ad8" + +# Propagators Usage Methods + + +def get_as_list(dict_object, key): + value = dict_object.get(key) + return [value] if value is not None else [] + + +# Inject Methods + + +def build_test_context( + trace_id=int(TRACE_ID_BASE16, 16), + span_id=int(SPAN_ID_BASE16, 16), + is_remote=True, + trace_flags=DEFAULT_TRACE_OPTIONS, + trace_state=DEFAULT_TRACE_STATE, +): + return set_span_in_context( + trace_api.DefaultSpan( + SpanContext( + trace_id, span_id, is_remote, trace_flags, trace_state, + ) + ) + ) + + +def build_dict_with_xray_trace_header( + trace_id="{}{}{}{}{}".format( + AwsXRayFormat.TRACE_ID_VERSION, + AwsXRayFormat.TRACE_ID_DELIMITER, + TRACE_ID_BASE16[: AwsXRayFormat.TRACE_ID_FIRST_PART_LENGTH], + AwsXRayFormat.TRACE_ID_DELIMITER, + TRACE_ID_BASE16[AwsXRayFormat.TRACE_ID_FIRST_PART_LENGTH :], + ), + span_id=SPAN_ID_BASE16, + sampled="0", +): + carrier = CaseInsensitiveDict() + + carrier[AwsXRayFormat.TRACE_HEADER_KEY] = "".join( + [ + "{}{}{}{}".format( + AwsXRayFormat.TRACE_ID_KEY, + AwsXRayFormat.KEY_AND_VALUE_DELIMITER, + trace_id, + AwsXRayFormat.KV_PAIR_DELIMITER, + ), + "{}{}{}{}".format( + AwsXRayFormat.PARENT_ID_KEY, + AwsXRayFormat.KEY_AND_VALUE_DELIMITER, + span_id, + AwsXRayFormat.KV_PAIR_DELIMITER, + ), + "{}{}{}".format( + AwsXRayFormat.SAMPLED_FLAG_KEY, + AwsXRayFormat.KEY_AND_VALUE_DELIMITER, + sampled, + ), + ] + ) + + return carrier + + +# Extract Methods + + +def get_extracted_span_context(encompassing_context): + return trace_api.get_current_span(encompassing_context).get_span_context() + + +class AwsXRayPropagatorTest(unittest.TestCase): + carrier_setter = CaseInsensitiveDict.__setitem__ + carrier_getter = get_as_list + XRAY_PROPAGATOR = AwsXRayFormat() + + # Inject Tests + + def test_inject_into_non_sampled_context(self): + carrier = CaseInsensitiveDict() + + AwsXRayPropagatorTest.XRAY_PROPAGATOR.inject( + AwsXRayPropagatorTest.carrier_setter, carrier, build_test_context() + ) + + self.assertTrue( + set(carrier.items()).issubset( + set(build_dict_with_xray_trace_header().items()) + ), + "Failed to inject into context that was not yet sampled", + ) + + def test_inject_into_sampled_context(self): + carrier = CaseInsensitiveDict() + + AwsXRayPropagatorTest.XRAY_PROPAGATOR.inject( + AwsXRayPropagatorTest.carrier_setter, + carrier, + build_test_context(trace_flags=TraceFlags(TraceFlags.SAMPLED)), + ) + + self.assertTrue( + set(carrier.items()).issubset( + set(build_dict_with_xray_trace_header(sampled="1").items(),) + ), + "Failed to inject into context that was already sampled", + ) + + def test_inject_into_context_with_non_default_state(self): + carrier = CaseInsensitiveDict() + + AwsXRayPropagatorTest.XRAY_PROPAGATOR.inject( + AwsXRayPropagatorTest.carrier_setter, + carrier, + build_test_context(trace_state=TraceState({"foo": "bar"})), + ) + + # TODO: (NathanielRN) Assert trace state when the propagator supports it + self.assertTrue( + set(carrier.items()).issubset( + set(build_dict_with_xray_trace_header().items(),) + ), + "Failed to inject into context with non default state", + ) + + # Extract Tests + + def test_extract_empty_carrier_from_none_carrier(self): + actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict() + ) + + self.assertEqual( + get_extracted_span_context(actual_context_encompassing_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_empty_carrier_from_invalid_context(self): + actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict() + ) + + self.assertEqual( + get_extracted_span_context(actual_context_encompassing_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_not_sampled_context(self): + actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + build_dict_with_xray_trace_header(), + ) + + self.assertEqual( + get_extracted_span_context(actual_context_encompassing_extracted), + get_extracted_span_context(build_test_context()), + ) + + def test_extract_sampled_context(self): + actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + build_dict_with_xray_trace_header(sampled="1"), + ) + + self.assertEqual( + get_extracted_span_context(actual_context_encompassing_extracted), + get_extracted_span_context( + build_test_context(trace_flags=TraceFlags(TraceFlags.SAMPLED)) + ), + ) + + def test_extract_different_order(self): + default_xray_trace_header_dict = build_dict_with_xray_trace_header() + trace_header_components = default_xray_trace_header_dict[ + AwsXRayFormat.TRACE_HEADER_KEY + ].split(AwsXRayFormat.KV_PAIR_DELIMITER) + reversed_trace_header_components = AwsXRayFormat.KV_PAIR_DELIMITER.join( + trace_header_components[::-1] + ) + xray_trace_header_dict_in_different_order = CaseInsensitiveDict( + {AwsXRayFormat.TRACE_HEADER_KEY: reversed_trace_header_components} + ) + actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + xray_trace_header_dict_in_different_order, + ) + + self.assertEqual( + get_extracted_span_context(actual_context_encompassing_extracted), + get_extracted_span_context(build_test_context()), + ) + + def test_extract_with_additional_fields(self): + default_xray_trace_header_dict = build_dict_with_xray_trace_header() + xray_trace_header_dict_with_extra_fields = CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: default_xray_trace_header_dict[ + AwsXRayFormat.TRACE_HEADER_KEY + ] + + ";Foo=Bar" + } + ) + actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + xray_trace_header_dict_with_extra_fields, + ) + + self.assertEqual( + get_extracted_span_context(actual_context_encompassing_extracted), + get_extracted_span_context(build_test_context()), + ) + + def test_extract_invalid_xray_trace_header(self): + actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + CaseInsensitiveDict({AwsXRayFormat.TRACE_HEADER_KEY: ""}), + ) + + self.assertEqual( + get_extracted_span_context(actual_context_encompassing_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_invalid_trace_id(self): + actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + build_dict_with_xray_trace_header( + trace_id="abcdefghijklmnopqrstuvwxyz123456" + ), + ) + + self.assertEqual( + get_extracted_span_context(actual_context_encompassing_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_invalid_trace_id_size(self): + actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + build_dict_with_xray_trace_header( + trace_id="1-8a3c60f7-d188f8fa79d48a391a778fa600" + ), + ) + + self.assertEqual( + get_extracted_span_context(actual_context_encompassing_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_invalid_span_id(self): + actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + build_dict_with_xray_trace_header(span_id="abcdefghijklmnop"), + ) + + self.assertEqual( + get_extracted_span_context(actual_context_encompassing_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_invalid_span_id_size(self): + actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + build_dict_with_xray_trace_header(span_id="53995c3f42cd8ad800"), + ) + + self.assertEqual( + get_extracted_span_context(actual_context_encompassing_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_invalid_empty_sampled_flag(self): + actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + build_dict_with_xray_trace_header(sampled=""), + ) + + self.assertEqual( + get_extracted_span_context(actual_context_encompassing_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_invalid_sampled_flag_size(self): + actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + build_dict_with_xray_trace_header(sampled="10002"), + ) + + self.assertEqual( + get_extracted_span_context(actual_context_encompassing_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_invalid_non_numeric_sampled_flag(self): + actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + build_dict_with_xray_trace_header(sampled="a"), + ) + + self.assertEqual( + get_extracted_span_context(actual_context_encompassing_extracted), + INVALID_SPAN_CONTEXT, + ) diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/test_aws_xray_ids_generator.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/test_aws_xray_ids_generator.py new file mode 100644 index 0000000000..1be1b68e82 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/test_aws_xray_ids_generator.py @@ -0,0 +1,43 @@ +# 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 datetime +import unittest + +from opentelemetry.sdk.extension.aws.trace import AwsXRayIdsGenerator +from opentelemetry.trace.span import INVALID_TRACE_ID + + +class AwsXRayIdsGeneratorTest(unittest.TestCase): + def test_ids_are_valid(self): + ids_generator = AwsXRayIdsGenerator() + for _ in range(1000): + trace_id = ids_generator.generate_trace_id() + self.assertTrue(trace_id != INVALID_TRACE_ID) + span_id = ids_generator.generate_span_id() + self.assertTrue(span_id != INVALID_TRACE_ID) + + def test_id_timestamps_are_acceptable_for_xray(self): + ids_generator = AwsXRayIdsGenerator() + for _ in range(1000): + trace_id = ids_generator.generate_trace_id() + trace_id_time = trace_id >> 96 + current_time = int(datetime.datetime.utcnow().timestamp()) + self.assertLessEqual(trace_id_time, current_time) + one_month_ago_time = int( + ( + datetime.datetime.utcnow() - datetime.timedelta(30) + ).timestamp() + ) + self.assertGreater(trace_id_time, one_month_ago_time) diff --git a/tox.ini b/tox.ini index 27d521eabb..9fd687be63 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,10 @@ envlist = ; Environments are organized by individual package, allowing ; for specifying supported Python versions per package. + ; opentelemetry-sdk-extension-aws + py3{5,6,7,8}-test-sdkextension-aws + pypy3-test-sdkextension-aws + ; opentelemetry-instrumentation-aiohttp-client py3{5,6,7,8}-test-instrumentation-aiohttp-client pypy3-test-instrumentation-aiohttp-client @@ -178,6 +182,7 @@ changedir = test-instrumentation-system-metrics: instrumentation/opentelemetry-instrumentation-system-metrics/tests test-instrumentation-tornado: instrumentation/opentelemetry-instrumentation-tornado/tests test-instrumentation-wsgi: instrumentation/opentelemetry-instrumentation-wsgi/tests + test-sdkextension-aws: sdk-extension/opentelemetry-sdk-extension-aws/tests test-exporter-datadog: exporter/opentelemetry-exporter-datadog/tests @@ -250,6 +255,9 @@ commands_pre = elasticsearch{2,5,6,7}: pip install {toxinidir}/opentelemetry-python-core/opentelemetry-instrumentation {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch[test] + + aws: pip install requests {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws + ; In order to get a healthy coverage report, ; we have to install packages in editable mode. coverage: python {toxinidir}/scripts/eachdist.py install --editable From ccb7a83922ee4b09a5b99611f406163680f8fa1b Mon Sep 17 00:00:00 2001 From: Nathaniel Ruiz Nowell Date: Sat, 7 Nov 2020 00:35:34 -0800 Subject: [PATCH 2/6] More pythonic propagator + package clean up --- .../opentelemetry-sdk-extension-aws/setup.cfg | 4 +- .../aws/trace/propagation/aws_xray_format.py | 191 +++++++++--------- .../trace/propagation/test_aws_xray_format.py | 35 ++++ 3 files changed, 132 insertions(+), 98 deletions(-) diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/setup.cfg b/sdk-extension/opentelemetry-sdk-extension-aws/setup.cfg index 4aa890ca53..6cfbe7cb68 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/setup.cfg +++ b/sdk-extension/opentelemetry-sdk-extension-aws/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.15.b0 + opentelemetry-api == 0.16.dev0 [options.entry_points] opentelemetry_propagator = @@ -47,7 +47,7 @@ opentelemetry_propagator = [options.extras_require] test = - opentelemetry-test == 0.15.b0 + opentelemetry-test == 0.16.dev0 [options.packages.find] where = src diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py index 17ef67ed9d..e544bd685c 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py @@ -55,10 +55,6 @@ class AwsXRayFormat(TextMapPropagator): IS_SAMPLED = "1" NOT_SAMPLED = "0" - # pylint: disable=too-many-locals - # pylint: disable=too-many-return-statements - # pylint: disable=too-many-branches - # pylint: disable=too-many-statements def extract( self, getter: Getter[TextMapPropagatorT], @@ -80,53 +76,66 @@ def extract( trace.INVALID_SPAN, context=context ) + trace_id, span_id, sampled, err = self.extract_span_properties( + trace_header + ) + + if err is not None: + return trace.set_span_in_context( + trace.INVALID_SPAN, context=context + ) + + options = 0 + if sampled: + options |= trace.TraceFlags.SAMPLED + + span_context = trace.SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=True, + trace_flags=trace.TraceFlags(options), + trace_state=trace.TraceState(), + ) + + if not span_context.is_valid: + _logger.error( + "Invalid Span Extracted. Insertting INVALID span into provided context." + ) + return trace.set_span_in_context( + trace.INVALID_SPAN, context=context + ) + + return trace.set_span_in_context( + trace.DefaultSpan(span_context), context=context + ) + + def extract_span_properties(self, trace_header): trace_id = trace.INVALID_TRACE_ID span_id = trace.INVALID_SPAN_ID sampled = False - next_kv_pair_start = 0 + extract_err = None - while next_kv_pair_start < len(trace_header): - try: - kv_pair_delimiter_index = trace_header.index( - self.KV_PAIR_DELIMITER, next_kv_pair_start - ) - kv_pair_subset = trace_header[ - next_kv_pair_start:kv_pair_delimiter_index - ] - next_kv_pair_start = kv_pair_delimiter_index + 1 - except ValueError: - kv_pair_subset = trace_header[next_kv_pair_start:] - next_kv_pair_start = len(trace_header) - - stripped_kv_pair = kv_pair_subset.strip() + for kv_pair_str in trace_header.split(self.KV_PAIR_DELIMITER): + if extract_err: + break try: - key_and_value_delimiter_index = stripped_kv_pair.index( + key_str, value_str = kv_pair_str.split( self.KEY_AND_VALUE_DELIMITER ) + key, value = key_str.strip(), value_str.strip() except ValueError: _logger.error( ( "Error parsing X-Ray trace header. Invalid key value pair: %s. Returning INVALID span context.", - kv_pair_subset, + kv_pair_str, ) ) - return trace.set_span_in_context( - trace.INVALID_SPAN, context=context - ) + return trace_id, span_id, sampled, extract_err - value = stripped_kv_pair[key_and_value_delimiter_index + 1 :] - - if stripped_kv_pair.startswith(self.TRACE_ID_KEY): - if ( - len(value) != self.TRACE_ID_LENGTH - or not value.startswith(self.TRACE_ID_VERSION) - or value[self.TRACE_ID_DELIMITER_INDEX_1] - != self.TRACE_ID_DELIMITER - or value[self.TRACE_ID_DELIMITER_INDEX_2] - != self.TRACE_ID_DELIMITER - ): + if key == self.TRACE_ID_KEY: + if not self.validate_trace_id(value): _logger.error( ( "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", @@ -134,19 +143,11 @@ def extract( trace_header, ) ) - return trace.set_span_in_context( - trace.INVALID_SPAN, context=context - ) + extract_err = True + break - timestamp_subset = value[ - self.TRACE_ID_DELIMITER_INDEX_1 - + 1 : self.TRACE_ID_DELIMITER_INDEX_2 - ] - unique_id_subset = value[ - self.TRACE_ID_DELIMITER_INDEX_2 + 1 : self.TRACE_ID_LENGTH - ] try: - trace_id = int(timestamp_subset + unique_id_subset, 16) + trace_id = self.parse_trace_id(value) except ValueError: _logger.error( ( @@ -155,11 +156,9 @@ def extract( trace_header, ) ) - return trace.set_span_in_context( - trace.INVALID_SPAN, context=context - ) - elif stripped_kv_pair.startswith(self.PARENT_ID_KEY): - if len(value) != self.PARENT_ID_LENGTH: + extract_err = True + elif key == self.PARENT_ID_KEY: + if not self.validate_span_id(value): _logger.error( ( "Invalid ParentId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", @@ -167,12 +166,11 @@ def extract( trace_header, ) ) - return trace.set_span_in_context( - trace.INVALID_SPAN, context=context - ) + extract_err = True + break try: - span_id = int(value, 16) + span_id = AwsXRayFormat.parse_span_id(value) except ValueError: _logger.error( ( @@ -181,25 +179,9 @@ def extract( trace_header, ) ) - return trace.set_span_in_context( - trace.INVALID_SPAN, context=context - ) - elif stripped_kv_pair.startswith(self.SAMPLED_FLAG_KEY): - is_sampled_flag_valid = True - - if len(value) != self.SAMPLED_FLAG_LENGTH: - is_sampled_flag_valid = False - - if is_sampled_flag_valid: - sampled_flag = value[0] - if sampled_flag == self.IS_SAMPLED: - sampled = True - elif sampled_flag == self.NOT_SAMPLED: - sampled = False - else: - is_sampled_flag_valid = False - - if not is_sampled_flag_valid: + extract_err = True + elif key == self.SAMPLED_FLAG_KEY: + if not self.validate_sampled_flag(value): _logger.error( ( "Invalid Sampling flag in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", @@ -207,34 +189,51 @@ def extract( trace_header, ) ) - return trace.set_span_in_context( - trace.INVALID_SPAN, context=context - ) + extract_err = True + break - options = 0 - if sampled: - options |= trace.TraceFlags.SAMPLED + sampled = self.parse_sampled_flag(value) - span_context = trace.SpanContext( - trace_id=trace_id, - span_id=span_id, - is_remote=True, - trace_flags=trace.TraceFlags(options), - trace_state=trace.TraceState(), - ) + return trace_id, span_id, sampled, extract_err - if not span_context.is_valid: - _logger.error( - "Invalid Span Extracted. Insertting INVALID span into provided context." - ) - return trace.set_span_in_context( - trace.INVALID_SPAN, context=context - ) + def validate_trace_id(self, trace_id_str): + return ( + len(trace_id_str) == self.TRACE_ID_LENGTH + and trace_id_str.startswith(self.TRACE_ID_VERSION) + and trace_id_str[self.TRACE_ID_DELIMITER_INDEX_1] + == self.TRACE_ID_DELIMITER + and trace_id_str[self.TRACE_ID_DELIMITER_INDEX_2] + == self.TRACE_ID_DELIMITER + ) - return trace.set_span_in_context( - trace.DefaultSpan(span_context), context=context + def parse_trace_id(self, trace_id_str): + timestamp_subset = trace_id_str[ + self.TRACE_ID_DELIMITER_INDEX_1 + + 1 : self.TRACE_ID_DELIMITER_INDEX_2 + ] + unique_id_subset = trace_id_str[ + self.TRACE_ID_DELIMITER_INDEX_2 + 1 : self.TRACE_ID_LENGTH + ] + return int(timestamp_subset + unique_id_subset, 16) + + def validate_span_id(self, span_id_str): + return len(span_id_str) == self.PARENT_ID_LENGTH + + @staticmethod + def parse_span_id(span_id_str): + return int(span_id_str, 16) + + def validate_sampled_flag(self, sampled_flag_str): + return len( + sampled_flag_str + ) == self.SAMPLED_FLAG_LENGTH and sampled_flag_str in ( + self.IS_SAMPLED, + self.NOT_SAMPLED, ) + def parse_sampled_flag(self, sampled_flag_str): + return sampled_flag_str[0] == self.IS_SAMPLED + def inject( self, set_in_carrier: Setter[TextMapPropagatorT], diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py index fc0946baba..0ea84ef217 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py @@ -247,6 +247,41 @@ def test_extract_with_additional_fields(self): get_extracted_span_context(build_test_context()), ) + def test_extract_with_extra_whitespace(self): + default_xray_trace_header_dict = build_dict_with_xray_trace_header() + trace_header_components = default_xray_trace_header_dict[ + AwsXRayFormat.TRACE_HEADER_KEY + ].split(AwsXRayFormat.KV_PAIR_DELIMITER) + xray_trace_header_dict_with_extra_whitespace = CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: AwsXRayFormat.KV_PAIR_DELIMITER.join( + [ + AwsXRayFormat.KEY_AND_VALUE_DELIMITER.join( + [ + " " + key + " ", + " " + value + " ", + ] + ) + for kv_pair_str in trace_header_components + for key, value in [ + kv_pair_str.split( + AwsXRayFormat.KEY_AND_VALUE_DELIMITER + ) + ] + ] + ) + } + ) + actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + xray_trace_header_dict_with_extra_whitespace, + ) + + self.assertEqual( + get_extracted_span_context(actual_context_encompassing_extracted), + get_extracted_span_context(build_test_context()), + ) + def test_extract_invalid_xray_trace_header(self): actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, From 26db369ec99ca3ac81536d0cce2c29418856aec5 Mon Sep 17 00:00:00 2001 From: Nathaniel Ruiz Nowell Date: Sun, 8 Nov 2020 12:46:21 -0800 Subject: [PATCH 3/6] Use an exception to catch inability to parse --- .../aws/trace/aws_xray_ids_generator.py | 11 ++- .../aws/trace/propagation/aws_xray_format.py | 86 +++++++++---------- .../tests/__init__.py | 13 --- .../tests/trace/__init__.py | 13 --- .../trace/test_aws_xray_ids_generator.py | 7 +- 5 files changed, 49 insertions(+), 81 deletions(-) diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/aws_xray_ids_generator.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/aws_xray_ids_generator.py index 799c16032a..aa7d51bc7d 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/aws_xray_ids_generator.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/aws_xray_ids_generator.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import datetime import random +import time from opentelemetry import trace @@ -28,10 +28,13 @@ class AwsXRayIdsGenerator(trace.IdsGenerator): See: https://docs.aws.amazon.com/xray/latest/devguide/xray-api-sendingdata.html#xray-api-traceids """ + random_ids_generator = trace.RandomIdsGenerator() + def generate_span_id(self) -> int: - return trace.RandomIdsGenerator().generate_span_id() + return self.random_ids_generator.generate_span_id() - def generate_trace_id(self) -> int: - trace_time = int(datetime.datetime.utcnow().timestamp()) + @staticmethod + def generate_trace_id() -> int: + trace_time = int(time.time()) trace_identifier = random.getrandbits(96) return (trace_time << 96) + trace_identifier diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py index e544bd685c..46c30a8d78 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py @@ -27,6 +27,12 @@ _logger = logging.getLogger(__name__) +class AwsParseTraceHeaderError(Exception): + def __init__(self, message): + super().__init__() + self.message = message + + class AwsXRayFormat(TextMapPropagator): """Propagator for the AWS X-Ray Trace Header propagation protocol. @@ -76,11 +82,12 @@ def extract( trace.INVALID_SPAN, context=context ) - trace_id, span_id, sampled, err = self.extract_span_properties( - trace_header - ) - - if err is not None: + try: + trace_id, span_id, sampled = self._extract_span_properties( + trace_header + ) + except AwsParseTraceHeaderError as err: + _logger.debug(err.message) return trace.set_span_in_context( trace.INVALID_SPAN, context=context ) @@ -98,7 +105,7 @@ def extract( ) if not span_context.is_valid: - _logger.error( + _logger.debug( "Invalid Span Extracted. Insertting INVALID span into provided context." ) return trace.set_span_in_context( @@ -109,94 +116,79 @@ def extract( trace.DefaultSpan(span_context), context=context ) - def extract_span_properties(self, trace_header): + def _extract_span_properties(self, trace_header): trace_id = trace.INVALID_TRACE_ID span_id = trace.INVALID_SPAN_ID sampled = False - extract_err = None - for kv_pair_str in trace_header.split(self.KV_PAIR_DELIMITER): - if extract_err: - break - try: key_str, value_str = kv_pair_str.split( self.KEY_AND_VALUE_DELIMITER ) key, value = key_str.strip(), value_str.strip() - except ValueError: - _logger.error( + except ValueError as ex: + raise AwsParseTraceHeaderError( ( "Error parsing X-Ray trace header. Invalid key value pair: %s. Returning INVALID span context.", kv_pair_str, ) - ) - return trace_id, span_id, sampled, extract_err - + ) from ex if key == self.TRACE_ID_KEY: - if not self.validate_trace_id(value): - _logger.error( + if not self._validate_trace_id(value): + raise AwsParseTraceHeaderError( ( "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", self.TRACE_HEADER_KEY, trace_header, ) ) - extract_err = True - break try: - trace_id = self.parse_trace_id(value) - except ValueError: - _logger.error( + trace_id = self._parse_trace_id(value) + except ValueError as ex: + raise AwsParseTraceHeaderError( ( "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", self.TRACE_HEADER_KEY, trace_header, ) - ) - extract_err = True + ) from ex elif key == self.PARENT_ID_KEY: - if not self.validate_span_id(value): - _logger.error( + if not self._validate_span_id(value): + raise AwsParseTraceHeaderError( ( "Invalid ParentId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", self.TRACE_HEADER_KEY, trace_header, ) ) - extract_err = True - break try: - span_id = AwsXRayFormat.parse_span_id(value) - except ValueError: - _logger.error( + span_id = AwsXRayFormat._parse_span_id(value) + except ValueError as ex: + raise AwsParseTraceHeaderError( ( "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", self.TRACE_HEADER_KEY, trace_header, ) - ) - extract_err = True + ) from ex elif key == self.SAMPLED_FLAG_KEY: - if not self.validate_sampled_flag(value): - _logger.error( + if not self._validate_sampled_flag(value): + raise AwsParseTraceHeaderError( ( "Invalid Sampling flag in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", self.TRACE_HEADER_KEY, trace_header, ) ) - extract_err = True - break - sampled = self.parse_sampled_flag(value) + sampled = self._parse_sampled_flag(value) - return trace_id, span_id, sampled, extract_err + return trace_id, span_id, sampled - def validate_trace_id(self, trace_id_str): + def _validate_trace_id(self, trace_id_str): return ( len(trace_id_str) == self.TRACE_ID_LENGTH and trace_id_str.startswith(self.TRACE_ID_VERSION) @@ -206,7 +198,7 @@ def validate_trace_id(self, trace_id_str): == self.TRACE_ID_DELIMITER ) - def parse_trace_id(self, trace_id_str): + def _parse_trace_id(self, trace_id_str): timestamp_subset = trace_id_str[ self.TRACE_ID_DELIMITER_INDEX_1 + 1 : self.TRACE_ID_DELIMITER_INDEX_2 @@ -216,14 +208,14 @@ def parse_trace_id(self, trace_id_str): ] return int(timestamp_subset + unique_id_subset, 16) - def validate_span_id(self, span_id_str): + def _validate_span_id(self, span_id_str): return len(span_id_str) == self.PARENT_ID_LENGTH @staticmethod - def parse_span_id(span_id_str): + def _parse_span_id(span_id_str): return int(span_id_str, 16) - def validate_sampled_flag(self, sampled_flag_str): + def _validate_sampled_flag(self, sampled_flag_str): return len( sampled_flag_str ) == self.SAMPLED_FLAG_LENGTH and sampled_flag_str in ( @@ -231,7 +223,7 @@ def validate_sampled_flag(self, sampled_flag_str): self.NOT_SAMPLED, ) - def parse_sampled_flag(self, sampled_flag_str): + def _parse_sampled_flag(self, sampled_flag_str): return sampled_flag_str[0] == self.IS_SAMPLED def inject( diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/__init__.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/__init__.py index b0a6f42841..e69de29bb2 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/__init__.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/__init__.py @@ -1,13 +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. diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/__init__.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/__init__.py index b0a6f42841..e69de29bb2 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/__init__.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/__init__.py @@ -1,13 +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. diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/test_aws_xray_ids_generator.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/test_aws_xray_ids_generator.py index 1be1b68e82..d914a5f241 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/test_aws_xray_ids_generator.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/test_aws_xray_ids_generator.py @@ -13,6 +13,7 @@ # limitations under the License. import datetime +import time import unittest from opentelemetry.sdk.extension.aws.trace import AwsXRayIdsGenerator @@ -33,11 +34,9 @@ def test_id_timestamps_are_acceptable_for_xray(self): for _ in range(1000): trace_id = ids_generator.generate_trace_id() trace_id_time = trace_id >> 96 - current_time = int(datetime.datetime.utcnow().timestamp()) + current_time = int(time.time()) self.assertLessEqual(trace_id_time, current_time) one_month_ago_time = int( - ( - datetime.datetime.utcnow() - datetime.timedelta(30) - ).timestamp() + (datetime.datetime.now() - datetime.timedelta(30)).timestamp() ) self.assertGreater(trace_id_time, one_month_ago_time) From ea0988a26a78a01ad277d6ef90255facd5b98bfa Mon Sep 17 00:00:00 2001 From: Nathaniel Ruiz Nowell Date: Sun, 8 Nov 2020 18:30:57 -0800 Subject: [PATCH 4/6] Make AWS Propagator tests more readable --- .../trace/propagation/test_aws_xray_format.py | 302 ++++++++---------- 1 file changed, 142 insertions(+), 160 deletions(-) diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py index 0ea84ef217..d16bf598d9 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py @@ -45,7 +45,7 @@ def get_as_list(dict_object, key): # Inject Methods -def build_test_context( +def build_test_current_context( trace_id=int(TRACE_ID_BASE16, 16), span_id=int(SPAN_ID_BASE16, 16), is_remote=True, @@ -54,56 +54,31 @@ def build_test_context( ): return set_span_in_context( trace_api.DefaultSpan( - SpanContext( - trace_id, span_id, is_remote, trace_flags, trace_state, + build_test_span_context( + trace_id, span_id, is_remote, trace_flags, trace_state ) ) ) -def build_dict_with_xray_trace_header( - trace_id="{}{}{}{}{}".format( - AwsXRayFormat.TRACE_ID_VERSION, - AwsXRayFormat.TRACE_ID_DELIMITER, - TRACE_ID_BASE16[: AwsXRayFormat.TRACE_ID_FIRST_PART_LENGTH], - AwsXRayFormat.TRACE_ID_DELIMITER, - TRACE_ID_BASE16[AwsXRayFormat.TRACE_ID_FIRST_PART_LENGTH :], - ), - span_id=SPAN_ID_BASE16, - sampled="0", -): - carrier = CaseInsensitiveDict() - - carrier[AwsXRayFormat.TRACE_HEADER_KEY] = "".join( - [ - "{}{}{}{}".format( - AwsXRayFormat.TRACE_ID_KEY, - AwsXRayFormat.KEY_AND_VALUE_DELIMITER, - trace_id, - AwsXRayFormat.KV_PAIR_DELIMITER, - ), - "{}{}{}{}".format( - AwsXRayFormat.PARENT_ID_KEY, - AwsXRayFormat.KEY_AND_VALUE_DELIMITER, - span_id, - AwsXRayFormat.KV_PAIR_DELIMITER, - ), - "{}{}{}".format( - AwsXRayFormat.SAMPLED_FLAG_KEY, - AwsXRayFormat.KEY_AND_VALUE_DELIMITER, - sampled, - ), - ] - ) +# Extract Methods - return carrier +def get_nested_span_context(parent_context): + return trace_api.get_current_span(parent_context).get_span_context() -# Extract Methods +# Helper Methods -def get_extracted_span_context(encompassing_context): - return trace_api.get_current_span(encompassing_context).get_span_context() + +def build_test_span_context( + trace_id=int(TRACE_ID_BASE16, 16), + span_id=int(SPAN_ID_BASE16, 16), + is_remote=True, + trace_flags=DEFAULT_TRACE_OPTIONS, + trace_state=DEFAULT_TRACE_STATE, +): + return SpanContext(trace_id, span_id, is_remote, trace_flags, trace_state,) class AwsXRayPropagatorTest(unittest.TestCase): @@ -117,259 +92,266 @@ def test_inject_into_non_sampled_context(self): carrier = CaseInsensitiveDict() AwsXRayPropagatorTest.XRAY_PROPAGATOR.inject( - AwsXRayPropagatorTest.carrier_setter, carrier, build_test_context() + AwsXRayPropagatorTest.carrier_setter, + carrier, + build_test_current_context(), ) - self.assertTrue( - set(carrier.items()).issubset( - set(build_dict_with_xray_trace_header().items()) - ), - "Failed to inject into context that was not yet sampled", + injected_items = set(carrier.items()) + expected_items = set( + CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0" + } + ).items() ) + self.assertEqual(injected_items, expected_items) + def test_inject_into_sampled_context(self): carrier = CaseInsensitiveDict() AwsXRayPropagatorTest.XRAY_PROPAGATOR.inject( AwsXRayPropagatorTest.carrier_setter, carrier, - build_test_context(trace_flags=TraceFlags(TraceFlags.SAMPLED)), + build_test_current_context( + trace_flags=TraceFlags(TraceFlags.SAMPLED) + ), ) - self.assertTrue( - set(carrier.items()).issubset( - set(build_dict_with_xray_trace_header(sampled="1").items(),) - ), - "Failed to inject into context that was already sampled", + injected_items = set(carrier.items()) + expected_items = set( + CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1" + } + ).items() ) + self.assertEqual(injected_items, expected_items) + def test_inject_into_context_with_non_default_state(self): carrier = CaseInsensitiveDict() AwsXRayPropagatorTest.XRAY_PROPAGATOR.inject( AwsXRayPropagatorTest.carrier_setter, carrier, - build_test_context(trace_state=TraceState({"foo": "bar"})), + build_test_current_context(trace_state=TraceState({"foo": "bar"})), ) # TODO: (NathanielRN) Assert trace state when the propagator supports it - self.assertTrue( - set(carrier.items()).issubset( - set(build_dict_with_xray_trace_header().items(),) - ), - "Failed to inject into context with non default state", + injected_items = set(carrier.items()) + expected_items = set( + CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0" + } + ).items() ) - # Extract Tests - - def test_extract_empty_carrier_from_none_carrier(self): - actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( - AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict() - ) + self.assertEqual(injected_items, expected_items) - self.assertEqual( - get_extracted_span_context(actual_context_encompassing_extracted), - INVALID_SPAN_CONTEXT, - ) + # Extract Tests def test_extract_empty_carrier_from_invalid_context(self): - actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict() ) self.assertEqual( - get_extracted_span_context(actual_context_encompassing_extracted), + get_nested_span_context(context_with_extracted), INVALID_SPAN_CONTEXT, ) def test_extract_not_sampled_context(self): - actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, - build_dict_with_xray_trace_header(), + CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0" + } + ), ) self.assertEqual( - get_extracted_span_context(actual_context_encompassing_extracted), - get_extracted_span_context(build_test_context()), + get_nested_span_context(context_with_extracted), + build_test_span_context(), ) def test_extract_sampled_context(self): - actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, - build_dict_with_xray_trace_header(sampled="1"), + CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1" + } + ), ) self.assertEqual( - get_extracted_span_context(actual_context_encompassing_extracted), - get_extracted_span_context( - build_test_context(trace_flags=TraceFlags(TraceFlags.SAMPLED)) + get_nested_span_context(context_with_extracted), + build_test_span_context( + trace_flags=TraceFlags(TraceFlags.SAMPLED) ), ) def test_extract_different_order(self): - default_xray_trace_header_dict = build_dict_with_xray_trace_header() - trace_header_components = default_xray_trace_header_dict[ - AwsXRayFormat.TRACE_HEADER_KEY - ].split(AwsXRayFormat.KV_PAIR_DELIMITER) - reversed_trace_header_components = AwsXRayFormat.KV_PAIR_DELIMITER.join( - trace_header_components[::-1] - ) - xray_trace_header_dict_in_different_order = CaseInsensitiveDict( - {AwsXRayFormat.TRACE_HEADER_KEY: reversed_trace_header_components} - ) - actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, - xray_trace_header_dict_in_different_order, + CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: "Sampled=0;Parent=53995c3f42cd8ad8;Root=1-8a3c60f7-d188f8fa79d48a391a778fa6" + } + ), ) self.assertEqual( - get_extracted_span_context(actual_context_encompassing_extracted), - get_extracted_span_context(build_test_context()), + get_nested_span_context(context_with_extracted), + build_test_span_context(), ) def test_extract_with_additional_fields(self): - default_xray_trace_header_dict = build_dict_with_xray_trace_header() - xray_trace_header_dict_with_extra_fields = CaseInsensitiveDict( - { - AwsXRayFormat.TRACE_HEADER_KEY: default_xray_trace_header_dict[ - AwsXRayFormat.TRACE_HEADER_KEY - ] - + ";Foo=Bar" - } - ) - actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, - xray_trace_header_dict_with_extra_fields, + CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0;Foo=Bar" + } + ), ) self.assertEqual( - get_extracted_span_context(actual_context_encompassing_extracted), - get_extracted_span_context(build_test_context()), + get_nested_span_context(context_with_extracted), + build_test_span_context(), ) def test_extract_with_extra_whitespace(self): - default_xray_trace_header_dict = build_dict_with_xray_trace_header() - trace_header_components = default_xray_trace_header_dict[ - AwsXRayFormat.TRACE_HEADER_KEY - ].split(AwsXRayFormat.KV_PAIR_DELIMITER) - xray_trace_header_dict_with_extra_whitespace = CaseInsensitiveDict( - { - AwsXRayFormat.TRACE_HEADER_KEY: AwsXRayFormat.KV_PAIR_DELIMITER.join( - [ - AwsXRayFormat.KEY_AND_VALUE_DELIMITER.join( - [ - " " + key + " ", - " " + value + " ", - ] - ) - for kv_pair_str in trace_header_components - for key, value in [ - kv_pair_str.split( - AwsXRayFormat.KEY_AND_VALUE_DELIMITER - ) - ] - ] - ) - } - ) - actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, - xray_trace_header_dict_with_extra_whitespace, + CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: " Root = 1-8a3c60f7-d188f8fa79d48a391a778fa6 ; Parent = 53995c3f42cd8ad8 ; Sampled = 0 " + } + ), ) self.assertEqual( - get_extracted_span_context(actual_context_encompassing_extracted), - get_extracted_span_context(build_test_context()), + get_nested_span_context(context_with_extracted), + build_test_span_context(), ) def test_extract_invalid_xray_trace_header(self): - actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict({AwsXRayFormat.TRACE_HEADER_KEY: ""}), ) self.assertEqual( - get_extracted_span_context(actual_context_encompassing_extracted), + get_nested_span_context(context_with_extracted), INVALID_SPAN_CONTEXT, ) def test_extract_invalid_trace_id(self): - actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, - build_dict_with_xray_trace_header( - trace_id="abcdefghijklmnopqrstuvwxyz123456" + CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-12345678-abcdefghijklmnopqrstuvwx;Parent=53995c3f42cd8ad8;Sampled=0" + } ), ) self.assertEqual( - get_extracted_span_context(actual_context_encompassing_extracted), + get_nested_span_context(context_with_extracted), INVALID_SPAN_CONTEXT, ) def test_extract_invalid_trace_id_size(self): - actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, - build_dict_with_xray_trace_header( - trace_id="1-8a3c60f7-d188f8fa79d48a391a778fa600" + CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa600;Parent=53995c3f42cd8ad8;Sampled=0=" + } ), ) self.assertEqual( - get_extracted_span_context(actual_context_encompassing_extracted), + get_nested_span_context(context_with_extracted), INVALID_SPAN_CONTEXT, ) def test_extract_invalid_span_id(self): - actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, - build_dict_with_xray_trace_header(span_id="abcdefghijklmnop"), + CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=abcdefghijklmnop;Sampled=0" + } + ), ) self.assertEqual( - get_extracted_span_context(actual_context_encompassing_extracted), + get_nested_span_context(context_with_extracted), INVALID_SPAN_CONTEXT, ) def test_extract_invalid_span_id_size(self): - actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, - build_dict_with_xray_trace_header(span_id="53995c3f42cd8ad800"), + CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad800;Sampled=0" + } + ), ) self.assertEqual( - get_extracted_span_context(actual_context_encompassing_extracted), + get_nested_span_context(context_with_extracted), INVALID_SPAN_CONTEXT, ) def test_extract_invalid_empty_sampled_flag(self): - actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, - build_dict_with_xray_trace_header(sampled=""), + CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=" + } + ), ) self.assertEqual( - get_extracted_span_context(actual_context_encompassing_extracted), + get_nested_span_context(context_with_extracted), INVALID_SPAN_CONTEXT, ) def test_extract_invalid_sampled_flag_size(self): - actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, - build_dict_with_xray_trace_header(sampled="10002"), + CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=011" + } + ), ) self.assertEqual( - get_extracted_span_context(actual_context_encompassing_extracted), + get_nested_span_context(context_with_extracted), INVALID_SPAN_CONTEXT, ) def test_extract_invalid_non_numeric_sampled_flag(self): - actual_context_encompassing_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, - build_dict_with_xray_trace_header(sampled="a"), + CaseInsensitiveDict( + { + AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=a" + } + ), ) self.assertEqual( - get_extracted_span_context(actual_context_encompassing_extracted), + get_nested_span_context(context_with_extracted), INVALID_SPAN_CONTEXT, ) From c0a3ea96d9775ba691b94d0271d0b5e5ff64692d Mon Sep 17 00:00:00 2001 From: Nathaniel Ruiz Nowell Date: Mon, 9 Nov 2020 12:55:20 -0800 Subject: [PATCH 5/6] Expose AWS Progagator variables and update readme --- .../README.rst | 2 +- .../aws/trace/propagation/aws_xray_format.py | 159 +++++++++--------- .../trace/propagation/test_aws_xray_format.py | 36 ++-- 3 files changed, 101 insertions(+), 96 deletions(-) diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/README.rst b/sdk-extension/opentelemetry-sdk-extension-aws/README.rst index 806193ab4a..8fd5b683d5 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/README.rst +++ b/sdk-extension/opentelemetry-sdk-extension-aws/README.rst @@ -41,7 +41,7 @@ Propagator: :: - export OTEL_PYTHON_PROPAGATORS = aws_xray + export OTEL_PROPAGATORS = aws_xray References diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py index 46c30a8d78..96a48e36cc 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py @@ -24,6 +24,27 @@ TextMapPropagatorT, ) +TRACE_HEADER_KEY = "X-Amzn-Trace-Id" +KV_PAIR_DELIMITER = ";" +KEY_AND_VALUE_DELIMITER = "=" + +TRACE_ID_KEY = "Root" +TRACE_ID_LENGTH = 35 +TRACE_ID_VERSION = "1" +TRACE_ID_DELIMITER = "-" +TRACE_ID_DELIMITER_INDEX_1 = 1 +TRACE_ID_DELIMITER_INDEX_2 = 10 +TRACE_ID_FIRST_PART_LENGTH = 8 + +PARENT_ID_KEY = "Parent" +PARENT_ID_LENGTH = 16 + +SAMPLED_FLAG_KEY = "Sampled" +SAMPLED_FLAG_LENGTH = 1 +IS_SAMPLED = "1" +NOT_SAMPLED = "0" + + _logger = logging.getLogger(__name__) @@ -40,26 +61,6 @@ class AwsXRayFormat(TextMapPropagator): """ # AWS - TRACE_HEADER_KEY = "X-Amzn-Trace-Id" - - KV_PAIR_DELIMITER = ";" - KEY_AND_VALUE_DELIMITER = "=" - - TRACE_ID_KEY = "Root" - TRACE_ID_LENGTH = 35 - TRACE_ID_VERSION = "1" - TRACE_ID_DELIMITER = "-" - TRACE_ID_DELIMITER_INDEX_1 = 1 - TRACE_ID_DELIMITER_INDEX_2 = 10 - TRACE_ID_FIRST_PART_LENGTH = 8 - - PARENT_ID_KEY = "Parent" - PARENT_ID_LENGTH = 16 - - SAMPLED_FLAG_KEY = "Sampled" - SAMPLED_FLAG_LENGTH = 1 - IS_SAMPLED = "1" - NOT_SAMPLED = "0" def extract( self, @@ -67,8 +68,7 @@ def extract( carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: - trace_header_list = getter(carrier, self.TRACE_HEADER_KEY) - trace_header_list = getter.get(carrier, self.TRACE_HEADER_KEY) + trace_header_list = getter.get(carrier, TRACE_HEADER_KEY) if not trace_header_list or len(trace_header_list) != 1: return trace.set_span_in_context( @@ -83,9 +83,11 @@ def extract( ) try: - trace_id, span_id, sampled = self._extract_span_properties( - trace_header - ) + ( + trace_id, + span_id, + sampled, + ) = AwsXRayFormat._extract_span_properties(trace_header) except AwsParseTraceHeaderError as err: _logger.debug(err.message) return trace.set_span_in_context( @@ -116,16 +118,15 @@ def extract( trace.DefaultSpan(span_context), context=context ) - def _extract_span_properties(self, trace_header): + @staticmethod + def _extract_span_properties(trace_header): trace_id = trace.INVALID_TRACE_ID span_id = trace.INVALID_SPAN_ID sampled = False - for kv_pair_str in trace_header.split(self.KV_PAIR_DELIMITER): + for kv_pair_str in trace_header.split(KV_PAIR_DELIMITER): try: - key_str, value_str = kv_pair_str.split( - self.KEY_AND_VALUE_DELIMITER - ) + key_str, value_str = kv_pair_str.split(KEY_AND_VALUE_DELIMITER) key, value = key_str.strip(), value_str.strip() except ValueError as ex: raise AwsParseTraceHeaderError( @@ -134,32 +135,32 @@ def _extract_span_properties(self, trace_header): kv_pair_str, ) ) from ex - if key == self.TRACE_ID_KEY: - if not self._validate_trace_id(value): + if key == TRACE_ID_KEY: + if not AwsXRayFormat._validate_trace_id(value): raise AwsParseTraceHeaderError( ( "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", - self.TRACE_HEADER_KEY, + TRACE_HEADER_KEY, trace_header, ) ) try: - trace_id = self._parse_trace_id(value) + trace_id = AwsXRayFormat._parse_trace_id(value) except ValueError as ex: raise AwsParseTraceHeaderError( ( "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", - self.TRACE_HEADER_KEY, + TRACE_HEADER_KEY, trace_header, ) ) from ex - elif key == self.PARENT_ID_KEY: - if not self._validate_span_id(value): + elif key == PARENT_ID_KEY: + if not AwsXRayFormat._validate_span_id(value): raise AwsParseTraceHeaderError( ( "Invalid ParentId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", - self.TRACE_HEADER_KEY, + TRACE_HEADER_KEY, trace_header, ) ) @@ -170,61 +171,63 @@ def _extract_span_properties(self, trace_header): raise AwsParseTraceHeaderError( ( "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", - self.TRACE_HEADER_KEY, + TRACE_HEADER_KEY, trace_header, ) ) from ex - elif key == self.SAMPLED_FLAG_KEY: - if not self._validate_sampled_flag(value): + elif key == SAMPLED_FLAG_KEY: + if not AwsXRayFormat._validate_sampled_flag(value): raise AwsParseTraceHeaderError( ( "Invalid Sampling flag in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", - self.TRACE_HEADER_KEY, + TRACE_HEADER_KEY, trace_header, ) ) - sampled = self._parse_sampled_flag(value) + sampled = AwsXRayFormat._parse_sampled_flag(value) return trace_id, span_id, sampled - def _validate_trace_id(self, trace_id_str): + @staticmethod + def _validate_trace_id(trace_id_str): return ( - len(trace_id_str) == self.TRACE_ID_LENGTH - and trace_id_str.startswith(self.TRACE_ID_VERSION) - and trace_id_str[self.TRACE_ID_DELIMITER_INDEX_1] - == self.TRACE_ID_DELIMITER - and trace_id_str[self.TRACE_ID_DELIMITER_INDEX_2] - == self.TRACE_ID_DELIMITER + len(trace_id_str) == TRACE_ID_LENGTH + and trace_id_str.startswith(TRACE_ID_VERSION) + and trace_id_str[TRACE_ID_DELIMITER_INDEX_1] == TRACE_ID_DELIMITER + and trace_id_str[TRACE_ID_DELIMITER_INDEX_2] == TRACE_ID_DELIMITER ) - def _parse_trace_id(self, trace_id_str): + @staticmethod + def _parse_trace_id(trace_id_str): timestamp_subset = trace_id_str[ - self.TRACE_ID_DELIMITER_INDEX_1 - + 1 : self.TRACE_ID_DELIMITER_INDEX_2 + TRACE_ID_DELIMITER_INDEX_1 + 1 : TRACE_ID_DELIMITER_INDEX_2 ] unique_id_subset = trace_id_str[ - self.TRACE_ID_DELIMITER_INDEX_2 + 1 : self.TRACE_ID_LENGTH + TRACE_ID_DELIMITER_INDEX_2 + 1 : TRACE_ID_LENGTH ] return int(timestamp_subset + unique_id_subset, 16) - def _validate_span_id(self, span_id_str): - return len(span_id_str) == self.PARENT_ID_LENGTH + @staticmethod + def _validate_span_id(span_id_str): + return len(span_id_str) == PARENT_ID_LENGTH @staticmethod def _parse_span_id(span_id_str): return int(span_id_str, 16) - def _validate_sampled_flag(self, sampled_flag_str): + @staticmethod + def _validate_sampled_flag(sampled_flag_str): return len( sampled_flag_str - ) == self.SAMPLED_FLAG_LENGTH and sampled_flag_str in ( - self.IS_SAMPLED, - self.NOT_SAMPLED, + ) == SAMPLED_FLAG_LENGTH and sampled_flag_str in ( + IS_SAMPLED, + NOT_SAMPLED, ) - def _parse_sampled_flag(self, sampled_flag_str): - return sampled_flag_str[0] == self.IS_SAMPLED + @staticmethod + def _parse_sampled_flag(sampled_flag_str): + return sampled_flag_str[0] == IS_SAMPLED def inject( self, @@ -240,37 +243,37 @@ def inject( otel_trace_id = "{:032x}".format(span_context.trace_id) xray_trace_id = ( - self.TRACE_ID_VERSION - + self.TRACE_ID_DELIMITER - + otel_trace_id[: self.TRACE_ID_FIRST_PART_LENGTH] - + self.TRACE_ID_DELIMITER - + otel_trace_id[self.TRACE_ID_FIRST_PART_LENGTH :] + TRACE_ID_VERSION + + TRACE_ID_DELIMITER + + otel_trace_id[:TRACE_ID_FIRST_PART_LENGTH] + + TRACE_ID_DELIMITER + + otel_trace_id[TRACE_ID_FIRST_PART_LENGTH:] ) parent_id = "{:016x}".format(span_context.span_id) sampling_flag = ( - self.IS_SAMPLED + IS_SAMPLED if span_context.trace_flags & trace.TraceFlags.SAMPLED - else self.NOT_SAMPLED + else NOT_SAMPLED ) # TODO: Add OT trace state to the X-Ray trace header trace_header = ( - self.TRACE_ID_KEY - + self.KEY_AND_VALUE_DELIMITER + TRACE_ID_KEY + + KEY_AND_VALUE_DELIMITER + xray_trace_id - + self.KV_PAIR_DELIMITER - + self.PARENT_ID_KEY - + self.KEY_AND_VALUE_DELIMITER + + KV_PAIR_DELIMITER + + PARENT_ID_KEY + + KEY_AND_VALUE_DELIMITER + parent_id - + self.KV_PAIR_DELIMITER - + self.SAMPLED_FLAG_KEY - + self.KEY_AND_VALUE_DELIMITER + + KV_PAIR_DELIMITER + + SAMPLED_FLAG_KEY + + KEY_AND_VALUE_DELIMITER + sampling_flag ) set_in_carrier( - carrier, self.TRACE_HEADER_KEY, trace_header, + carrier, TRACE_HEADER_KEY, trace_header, ) diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py index d16bf598d9..0fc72fc842 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py @@ -18,6 +18,7 @@ import opentelemetry.trace as trace_api from opentelemetry.sdk.extension.aws.trace.propagation.aws_xray_format import ( + TRACE_HEADER_KEY, AwsXRayFormat, ) from opentelemetry.trace import ( @@ -29,6 +30,7 @@ TraceState, set_span_in_context, ) +from opentelemetry.trace.propagation.textmap import DictGetter TRACE_ID_BASE16 = "8a3c60f7d188f8fa79d48a391a778fa6" @@ -83,7 +85,7 @@ def build_test_span_context( class AwsXRayPropagatorTest(unittest.TestCase): carrier_setter = CaseInsensitiveDict.__setitem__ - carrier_getter = get_as_list + carrier_getter = DictGetter() XRAY_PROPAGATOR = AwsXRayFormat() # Inject Tests @@ -101,7 +103,7 @@ def test_inject_into_non_sampled_context(self): expected_items = set( CaseInsensitiveDict( { - AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0" + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0" } ).items() ) @@ -123,7 +125,7 @@ def test_inject_into_sampled_context(self): expected_items = set( CaseInsensitiveDict( { - AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1" + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1" } ).items() ) @@ -144,7 +146,7 @@ def test_inject_into_context_with_non_default_state(self): expected_items = set( CaseInsensitiveDict( { - AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0" + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0" } ).items() ) @@ -168,7 +170,7 @@ def test_extract_not_sampled_context(self): AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict( { - AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0" + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0" } ), ) @@ -183,7 +185,7 @@ def test_extract_sampled_context(self): AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict( { - AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1" + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1" } ), ) @@ -200,7 +202,7 @@ def test_extract_different_order(self): AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict( { - AwsXRayFormat.TRACE_HEADER_KEY: "Sampled=0;Parent=53995c3f42cd8ad8;Root=1-8a3c60f7-d188f8fa79d48a391a778fa6" + TRACE_HEADER_KEY: "Sampled=0;Parent=53995c3f42cd8ad8;Root=1-8a3c60f7-d188f8fa79d48a391a778fa6" } ), ) @@ -215,7 +217,7 @@ def test_extract_with_additional_fields(self): AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict( { - AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0;Foo=Bar" + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0;Foo=Bar" } ), ) @@ -230,7 +232,7 @@ def test_extract_with_extra_whitespace(self): AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict( { - AwsXRayFormat.TRACE_HEADER_KEY: " Root = 1-8a3c60f7-d188f8fa79d48a391a778fa6 ; Parent = 53995c3f42cd8ad8 ; Sampled = 0 " + TRACE_HEADER_KEY: " Root = 1-8a3c60f7-d188f8fa79d48a391a778fa6 ; Parent = 53995c3f42cd8ad8 ; Sampled = 0 " } ), ) @@ -243,7 +245,7 @@ def test_extract_with_extra_whitespace(self): def test_extract_invalid_xray_trace_header(self): context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( AwsXRayPropagatorTest.carrier_getter, - CaseInsensitiveDict({AwsXRayFormat.TRACE_HEADER_KEY: ""}), + CaseInsensitiveDict({TRACE_HEADER_KEY: ""}), ) self.assertEqual( @@ -256,7 +258,7 @@ def test_extract_invalid_trace_id(self): AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict( { - AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-12345678-abcdefghijklmnopqrstuvwx;Parent=53995c3f42cd8ad8;Sampled=0" + TRACE_HEADER_KEY: "Root=1-12345678-abcdefghijklmnopqrstuvwx;Parent=53995c3f42cd8ad8;Sampled=0" } ), ) @@ -271,7 +273,7 @@ def test_extract_invalid_trace_id_size(self): AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict( { - AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa600;Parent=53995c3f42cd8ad8;Sampled=0=" + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa600;Parent=53995c3f42cd8ad8;Sampled=0=" } ), ) @@ -286,7 +288,7 @@ def test_extract_invalid_span_id(self): AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict( { - AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=abcdefghijklmnop;Sampled=0" + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=abcdefghijklmnop;Sampled=0" } ), ) @@ -301,7 +303,7 @@ def test_extract_invalid_span_id_size(self): AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict( { - AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad800;Sampled=0" + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad800;Sampled=0" } ), ) @@ -316,7 +318,7 @@ def test_extract_invalid_empty_sampled_flag(self): AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict( { - AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=" + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=" } ), ) @@ -331,7 +333,7 @@ def test_extract_invalid_sampled_flag_size(self): AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict( { - AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=011" + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=011" } ), ) @@ -346,7 +348,7 @@ def test_extract_invalid_non_numeric_sampled_flag(self): AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict( { - AwsXRayFormat.TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=a" + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=a" } ), ) From 87789cbb95a5fb863059b10e80a6d852808560e9 Mon Sep 17 00:00:00 2001 From: Nathaniel Ruiz Nowell Date: Mon, 9 Nov 2020 17:02:23 -0800 Subject: [PATCH 6/6] Use joins to create trace ids and headers --- .../aws/trace/propagation/aws_xray_format.py | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py index 96a48e36cc..c888775114 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py @@ -242,12 +242,12 @@ def inject( return otel_trace_id = "{:032x}".format(span_context.trace_id) - xray_trace_id = ( - TRACE_ID_VERSION - + TRACE_ID_DELIMITER - + otel_trace_id[:TRACE_ID_FIRST_PART_LENGTH] - + TRACE_ID_DELIMITER - + otel_trace_id[TRACE_ID_FIRST_PART_LENGTH:] + xray_trace_id = TRACE_ID_DELIMITER.join( + [ + TRACE_ID_VERSION, + otel_trace_id[:TRACE_ID_FIRST_PART_LENGTH], + otel_trace_id[TRACE_ID_FIRST_PART_LENGTH:], + ] ) parent_id = "{:016x}".format(span_context.span_id) @@ -260,18 +260,15 @@ def inject( # TODO: Add OT trace state to the X-Ray trace header - trace_header = ( - TRACE_ID_KEY - + KEY_AND_VALUE_DELIMITER - + xray_trace_id - + KV_PAIR_DELIMITER - + PARENT_ID_KEY - + KEY_AND_VALUE_DELIMITER - + parent_id - + KV_PAIR_DELIMITER - + SAMPLED_FLAG_KEY - + KEY_AND_VALUE_DELIMITER - + sampling_flag + trace_header = KV_PAIR_DELIMITER.join( + [ + KEY_AND_VALUE_DELIMITER.join([key, value]) + for key, value in [ + (TRACE_ID_KEY, xray_trace_id), + (PARENT_ID_KEY, parent_id), + (SAMPLED_FLAG_KEY, sampling_flag), + ] + ] ) set_in_carrier(