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..8fd5b683d5 --- /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_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..6cfbe7cb68 --- /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.16.dev0 + +[options.entry_points] +opentelemetry_propagator = + aws_xray = opentelemetry.sdk.extension.aws.trace.propagation.aws_xray_format:AwsXRayFormat + +[options.extras_require] +test = + opentelemetry-test == 0.16.dev0 + +[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..aa7d51bc7d --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/aws_xray_ids_generator.py @@ -0,0 +1,40 @@ +# 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 random +import time + +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 + """ + + random_ids_generator = trace.RandomIdsGenerator() + + def generate_span_id(self) -> int: + return self.random_ids_generator.generate_span_id() + + @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/__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..c888775114 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/propagation/aws_xray_format.py @@ -0,0 +1,276 @@ +# 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, +) + +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__) + + +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. + + See: https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader + """ + + # AWS + + def extract( + self, + getter: Getter[TextMapPropagatorT], + carrier: TextMapPropagatorT, + context: typing.Optional[Context] = None, + ) -> Context: + 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( + 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 + ) + + try: + ( + 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( + 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.debug( + "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 + ) + + @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(KV_PAIR_DELIMITER): + try: + 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( + ( + "Error parsing X-Ray trace header. Invalid key value pair: %s. Returning INVALID span context.", + kv_pair_str, + ) + ) from ex + 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.", + TRACE_HEADER_KEY, + trace_header, + ) + ) + + try: + 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.", + TRACE_HEADER_KEY, + trace_header, + ) + ) from ex + 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.", + TRACE_HEADER_KEY, + trace_header, + ) + ) + + try: + 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.", + TRACE_HEADER_KEY, + trace_header, + ) + ) from ex + 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.", + TRACE_HEADER_KEY, + trace_header, + ) + ) + + sampled = AwsXRayFormat._parse_sampled_flag(value) + + return trace_id, span_id, sampled + + @staticmethod + def _validate_trace_id(trace_id_str): + return ( + 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 + ) + + @staticmethod + def _parse_trace_id(trace_id_str): + timestamp_subset = trace_id_str[ + TRACE_ID_DELIMITER_INDEX_1 + 1 : TRACE_ID_DELIMITER_INDEX_2 + ] + unique_id_subset = trace_id_str[ + TRACE_ID_DELIMITER_INDEX_2 + 1 : TRACE_ID_LENGTH + ] + return int(timestamp_subset + unique_id_subset, 16) + + @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) + + @staticmethod + def _validate_sampled_flag(sampled_flag_str): + return len( + sampled_flag_str + ) == SAMPLED_FLAG_LENGTH and sampled_flag_str in ( + IS_SAMPLED, + NOT_SAMPLED, + ) + + @staticmethod + def _parse_sampled_flag(sampled_flag_str): + return sampled_flag_str[0] == IS_SAMPLED + + 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 = 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) + + sampling_flag = ( + IS_SAMPLED + if span_context.trace_flags & trace.TraceFlags.SAMPLED + else NOT_SAMPLED + ) + + # TODO: Add OT trace state to the X-Ray trace header + + 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( + carrier, 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..e69de29bb2 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..e69de29bb2 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..0fc72fc842 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/propagation/test_aws_xray_format.py @@ -0,0 +1,359 @@ +# 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 ( + TRACE_HEADER_KEY, + AwsXRayFormat, +) +from opentelemetry.trace import ( + DEFAULT_TRACE_OPTIONS, + DEFAULT_TRACE_STATE, + INVALID_SPAN_CONTEXT, + SpanContext, + TraceFlags, + TraceState, + set_span_in_context, +) +from opentelemetry.trace.propagation.textmap import DictGetter + +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_current_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( + build_test_span_context( + trace_id, span_id, is_remote, trace_flags, trace_state + ) + ) + ) + + +# Extract Methods + + +def get_nested_span_context(parent_context): + return trace_api.get_current_span(parent_context).get_span_context() + + +# Helper Methods + + +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): + carrier_setter = CaseInsensitiveDict.__setitem__ + carrier_getter = DictGetter() + 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_current_context(), + ) + + injected_items = set(carrier.items()) + expected_items = set( + CaseInsensitiveDict( + { + 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_current_context( + trace_flags=TraceFlags(TraceFlags.SAMPLED) + ), + ) + + injected_items = set(carrier.items()) + expected_items = set( + CaseInsensitiveDict( + { + 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_current_context(trace_state=TraceState({"foo": "bar"})), + ) + + # TODO: (NathanielRN) Assert trace state when the propagator supports it + injected_items = set(carrier.items()) + expected_items = set( + CaseInsensitiveDict( + { + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0" + } + ).items() + ) + + self.assertEqual(injected_items, expected_items) + + # Extract Tests + + def test_extract_empty_carrier_from_invalid_context(self): + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, CaseInsensitiveDict() + ) + + self.assertEqual( + get_nested_span_context(context_with_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_not_sampled_context(self): + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + CaseInsensitiveDict( + { + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0" + } + ), + ) + + self.assertEqual( + get_nested_span_context(context_with_extracted), + build_test_span_context(), + ) + + def test_extract_sampled_context(self): + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + CaseInsensitiveDict( + { + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1" + } + ), + ) + + self.assertEqual( + get_nested_span_context(context_with_extracted), + build_test_span_context( + trace_flags=TraceFlags(TraceFlags.SAMPLED) + ), + ) + + def test_extract_different_order(self): + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + CaseInsensitiveDict( + { + TRACE_HEADER_KEY: "Sampled=0;Parent=53995c3f42cd8ad8;Root=1-8a3c60f7-d188f8fa79d48a391a778fa6" + } + ), + ) + + self.assertEqual( + get_nested_span_context(context_with_extracted), + build_test_span_context(), + ) + + def test_extract_with_additional_fields(self): + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + CaseInsensitiveDict( + { + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0;Foo=Bar" + } + ), + ) + + self.assertEqual( + get_nested_span_context(context_with_extracted), + build_test_span_context(), + ) + + def test_extract_with_extra_whitespace(self): + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + CaseInsensitiveDict( + { + TRACE_HEADER_KEY: " Root = 1-8a3c60f7-d188f8fa79d48a391a778fa6 ; Parent = 53995c3f42cd8ad8 ; Sampled = 0 " + } + ), + ) + + self.assertEqual( + get_nested_span_context(context_with_extracted), + build_test_span_context(), + ) + + def test_extract_invalid_xray_trace_header(self): + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + CaseInsensitiveDict({TRACE_HEADER_KEY: ""}), + ) + + self.assertEqual( + get_nested_span_context(context_with_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_invalid_trace_id(self): + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + CaseInsensitiveDict( + { + TRACE_HEADER_KEY: "Root=1-12345678-abcdefghijklmnopqrstuvwx;Parent=53995c3f42cd8ad8;Sampled=0" + } + ), + ) + + self.assertEqual( + get_nested_span_context(context_with_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_invalid_trace_id_size(self): + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + CaseInsensitiveDict( + { + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa600;Parent=53995c3f42cd8ad8;Sampled=0=" + } + ), + ) + + self.assertEqual( + get_nested_span_context(context_with_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_invalid_span_id(self): + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + CaseInsensitiveDict( + { + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=abcdefghijklmnop;Sampled=0" + } + ), + ) + + self.assertEqual( + get_nested_span_context(context_with_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_invalid_span_id_size(self): + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + CaseInsensitiveDict( + { + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad800;Sampled=0" + } + ), + ) + + self.assertEqual( + get_nested_span_context(context_with_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_invalid_empty_sampled_flag(self): + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + CaseInsensitiveDict( + { + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=" + } + ), + ) + + self.assertEqual( + get_nested_span_context(context_with_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_invalid_sampled_flag_size(self): + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + CaseInsensitiveDict( + { + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=011" + } + ), + ) + + self.assertEqual( + get_nested_span_context(context_with_extracted), + INVALID_SPAN_CONTEXT, + ) + + def test_extract_invalid_non_numeric_sampled_flag(self): + context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( + AwsXRayPropagatorTest.carrier_getter, + CaseInsensitiveDict( + { + TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=a" + } + ), + ) + + self.assertEqual( + get_nested_span_context(context_with_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..d914a5f241 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/test_aws_xray_ids_generator.py @@ -0,0 +1,42 @@ +# 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 time +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(time.time()) + self.assertLessEqual(trace_id_time, current_time) + one_month_ago_time = int( + (datetime.datetime.now() - 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