diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5e2dead6d..fc2c043d1 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 000000000..7a82dad1c --- /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 000000000..f49a4e16e --- /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 000000000..aed3e3327 --- /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 000000000..806193ab4 --- /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 000000000..4aa890ca5 --- /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 000000000..a5b080374 --- /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 000000000..f33a095c0 --- /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 000000000..799c16032 --- /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 000000000..e69de29bb 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 000000000..17ef67ed9 --- /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 000000000..1f98d44fa --- /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 000000000..b0a6f4284 --- /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 000000000..b0a6f4284 --- /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 000000000..e69de29bb 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 000000000..fc0946bab --- /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 000000000..1be1b68e8 --- /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 27d521eab..9fd687be6 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