diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md new file mode 100644 index 0000000000..479f2105e7 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -0,0 +1,42 @@ +# Changelog + +## Unreleased + +## Version 0.14b0 + +Released 2020-10-13 + +- Add timestamps to OTLP exporter + ([#1199](https://github.com/open-telemetry/opentelemetry-python/pull/1199)) +- Update OpenTelemetry protos to v0.5.0 + ([#1143](https://github.com/open-telemetry/opentelemetry-python/pull/1143)) + +## Version 0.13b0 + +Released 2020-09-17 + +- Add instrumentation info to exported spans + ([#1095](https://github.com/open-telemetry/opentelemetry-python/pull/1095)) +- Add metric OTLP exporter + ([#835](https://github.com/open-telemetry/opentelemetry-python/pull/835)) +- Add type hints to OTLP exporter + ([#1121](https://github.com/open-telemetry/opentelemetry-python/pull/1121)) + +## Version 0.12b0 + +- Change package name to opentelemetry-exporter-otlp + ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) +- Update default port to 55680 + ([#977](https://github.com/open-telemetry/opentelemetry-python/pull/977)) + +## Version 0.11b0 + +Released 2020-07-28 + +- Update span exporter to use OpenTelemetry Proto v0.4.0 ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/889)) + +## 0.9b0 + +Released 2020-06-10 + +- Initial release diff --git a/exporter/opentelemetry-exporter-otlp/LICENSE b/exporter/opentelemetry-exporter-otlp/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/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. diff --git a/exporter/opentelemetry-exporter-otlp/MANIFEST.in b/exporter/opentelemetry-exporter-otlp/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/MANIFEST.in @@ -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/exporter/opentelemetry-exporter-otlp/README.rst b/exporter/opentelemetry-exporter-otlp/README.rst new file mode 100644 index 0000000000..8adf657181 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/README.rst @@ -0,0 +1,25 @@ +OpenTelemetry Collector Exporter +================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp.svg + :target: https://pypi.org/project/opentelemetry-exporter-otlp/ + +This library allows to export data to the OpenTelemetry Collector using the OpenTelemetry Protocol. + +Installation +------------ + +:: + + pip install opentelemetry-exporter-otlp + + +References +---------- + +* `OpenTelemetry Collector Exporter `_ +* `OpenTelemetry Collector `_ +* `OpenTelemetry `_ +* `OpenTelemetry Protocol Specification `_ diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg new file mode 100644 index 0000000000..0f5870813e --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -0,0 +1,54 @@ +# 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-exporter-otlp +description = OpenTelemetry Collector Exporter +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/tree/master/exporter/opentelemetry-exporter-otlp +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 = + grpcio >= 1.0.0, < 2.0.0 + googleapis-common-protos ~= 1.52.0 + opentelemetry-api == 0.15.dev0 + opentelemetry-sdk == 0.15.dev0 + opentelemetry-proto == 0.15.dev0 + backoff ~= 1.10.0 + +[options.extras_require] +test = + pytest-grpc + +[options.packages.find] +where = src diff --git a/exporter/opentelemetry-exporter-otlp/setup.py b/exporter/opentelemetry-exporter-otlp/setup.py new file mode 100644 index 0000000000..c04c30fca4 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/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", "exporter", "otlp", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py new file mode 100644 index 0000000000..a4d8f46d4c --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/__init__.py @@ -0,0 +1,57 @@ +# 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. + + +""" +This library allows to export tracing data to an OTLP collector. + +Usage +----- + +The **OTLP Span Exporter** allows to export `OpenTelemetry`_ traces to the +`OTLP`_ collector. + + +.. _OTLP: https://github.com/open-telemetry/opentelemetry-collector/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ + +.. code:: python + + from opentelemetry import trace + from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter + from opentelemetry.sdk.resources import Resource + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + + # Resource can be required for some backends, e.g. Jaeger + # If resource wouldn't be set - traces wouldn't appears in Jaeger + resource = Resource(attributes={ + "service.name": "service" + }) + + trace.set_tracer_provider(TracerProvider(resource=resource))) + tracer = trace.get_tracer(__name__) + + otlp_exporter = OTLPSpanExporter(endpoint="localhost:55680") + + span_processor = BatchExportSpanProcessor(otlp_exporter) + + trace.get_tracer_provider().add_span_processor(span_processor) + + with tracer.start_as_current_span("foo"): + print("Hello world!") + +API +--- +""" diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py new file mode 100644 index 0000000000..079557f831 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -0,0 +1,211 @@ +# 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. + +"""OTLP Exporter""" + +import logging +from abc import ABC, abstractmethod +from collections.abc import Mapping, Sequence +from time import sleep +from typing import Any, Callable, Dict, Generic, List, Optional +from typing import Sequence as TypingSequence +from typing import Text, Tuple, TypeVar + +from backoff import expo +from google.rpc.error_details_pb2 import RetryInfo +from grpc import ( + ChannelCredentials, + RpcError, + StatusCode, + insecure_channel, + secure_channel, +) + +from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue +from opentelemetry.proto.resource.v1.resource_pb2 import Resource +from opentelemetry.sdk.resources import Resource as SDKResource + +logger = logging.getLogger(__name__) +SDKDataT = TypeVar("SDKDataT") +ResourceDataT = TypeVar("ResourceDataT") +TypingResourceT = TypeVar("TypingResourceT") +ExportServiceRequestT = TypeVar("ExportServiceRequestT") +ExportResultT = TypeVar("ExportResultT") + + +def _translate_key_values(key: Text, value: Any) -> KeyValue: + + if isinstance(value, bool): + any_value = AnyValue(bool_value=value) + + elif isinstance(value, str): + any_value = AnyValue(string_value=value) + + elif isinstance(value, int): + any_value = AnyValue(int_value=value) + + elif isinstance(value, float): + any_value = AnyValue(double_value=value) + + elif isinstance(value, Sequence): + any_value = AnyValue(array_value=value) + + elif isinstance(value, Mapping): + any_value = AnyValue(kvlist_value=value) + + else: + raise Exception( + "Invalid type {} of value {}".format(type(value), value) + ) + + return KeyValue(key=key, value=any_value) + + +def _get_resource_data( + sdk_resource_instrumentation_library_data: Dict[ + SDKResource, ResourceDataT + ], + resource_class: Callable[..., TypingResourceT], + name: str, +) -> List[TypingResourceT]: + + resource_data = [] + + for ( + sdk_resource, + instrumentation_library_data, + ) in sdk_resource_instrumentation_library_data.items(): + + collector_resource = Resource() + + for key, value in sdk_resource.attributes.items(): + + try: + # pylint: disable=no-member + collector_resource.attributes.append( + _translate_key_values(key, value) + ) + except Exception as error: # pylint: disable=broad-except + logger.exception(error) + + resource_data.append( + resource_class( + **{ + "resource": collector_resource, + "instrumentation_library_{}".format(name): [ + instrumentation_library_data + ], + } + ) + ) + + return resource_data + + +# pylint: disable=no-member +class OTLPExporterMixin( + ABC, Generic[SDKDataT, ExportServiceRequestT, ExportResultT] +): + """OTLP span/metric exporter + + Args: + endpoint: OpenTelemetry Collector receiver endpoint + credentials: ChannelCredentials object for server authentication + metadata: Metadata to send when exporting + """ + + def __init__( + self, + endpoint: str = "localhost:55680", + credentials: ChannelCredentials = None, + metadata: Optional[Tuple[Any]] = None, + ): + super().__init__() + + self._metadata = metadata + self._collector_span_kwargs = None + + if credentials is None: + self._client = self._stub(insecure_channel(endpoint)) + else: + self._client = self._stub(secure_channel(endpoint, credentials)) + + @abstractmethod + def _translate_data( + self, data: TypingSequence[SDKDataT] + ) -> ExportServiceRequestT: + pass + + def _export(self, data: TypingSequence[SDKDataT]) -> ExportResultT: + # expo returns a generator that yields delay values which grow + # exponentially. Once delay is greater than max_value, the yielded + # value will remain constant. + # max_value is set to 900 (900 seconds is 15 minutes) to use the same + # value as used in the Go implementation. + + max_value = 900 + + for delay in expo(max_value=max_value): + + if delay == max_value: + return self._result.FAILURE + + try: + self._client.Export( + request=self._translate_data(data), + metadata=self._metadata, + ) + + return self._result.SUCCESS + + except RpcError as error: + + if error.code() in [ + StatusCode.CANCELLED, + StatusCode.DEADLINE_EXCEEDED, + StatusCode.PERMISSION_DENIED, + StatusCode.UNAUTHENTICATED, + StatusCode.RESOURCE_EXHAUSTED, + StatusCode.ABORTED, + StatusCode.OUT_OF_RANGE, + StatusCode.UNAVAILABLE, + StatusCode.DATA_LOSS, + ]: + + retry_info_bin = dict(error.trailing_metadata()).get( + "google.rpc.retryinfo-bin" + ) + if retry_info_bin is not None: + retry_info = RetryInfo() + retry_info.ParseFromString(retry_info_bin) + delay = ( + retry_info.retry_delay.seconds + + retry_info.retry_delay.nanos / 1.0e9 + ) + + logger.debug( + "Waiting %ss before retrying export of span", delay + ) + sleep(delay) + continue + + if error.code() == StatusCode.OK: + return self._result.SUCCESS + + return self._result.FAILURE + + return self._result.FAILURE + + def shutdown(self) -> None: + pass diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py new file mode 100644 index 0000000000..40feb222fb --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py @@ -0,0 +1,248 @@ +# 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. + +"""OTLP Metrics Exporter""" + +import logging +from typing import List, Sequence, Type, TypeVar + +# pylint: disable=duplicate-code +from opentelemetry.exporter.otlp.exporter import ( + OTLPExporterMixin, + _get_resource_data, +) +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, +) +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc import ( + MetricsServiceStub, +) +from opentelemetry.proto.common.v1.common_pb2 import StringKeyValue +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + AggregationTemporality, + DoubleDataPoint, + DoubleGauge, + DoubleSum, + InstrumentationLibraryMetrics, + IntDataPoint, + IntGauge, + IntSum, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import Metric as OTLPMetric +from opentelemetry.proto.metrics.v1.metrics_pb2 import ResourceMetrics +from opentelemetry.sdk.metrics import ( + Counter, + SumObserver, + UpDownCounter, + UpDownSumObserver, + ValueObserver, + ValueRecorder, +) +from opentelemetry.sdk.metrics.export import ( + MetricRecord, + MetricsExporter, + MetricsExportResult, +) + +logger = logging.getLogger(__name__) +DataPointT = TypeVar("DataPointT", IntDataPoint, DoubleDataPoint) + + +def _get_data_points( + sdk_metric: MetricRecord, data_point_class: Type[DataPointT] +) -> List[DataPointT]: + + data_points = [] + + for ( + label, + bound_counter, + ) in sdk_metric.instrument.bound_instruments.items(): + + string_key_values = [] + + for label_key, label_value in label: + string_key_values.append( + StringKeyValue(key=label_key, value=label_value) + ) + + for view_data in bound_counter.view_datas: + + if view_data.labels == label: + + data_points.append( + data_point_class( + labels=string_key_values, + value=view_data.aggregator.current, + start_time_unix_nano=( + view_data.aggregator.last_checkpoint_timestamp + ), + time_unix_nano=( + view_data.aggregator.last_update_timestamp + ), + ) + ) + break + + return data_points + + +class OTLPMetricsExporter( + MetricsExporter, + OTLPExporterMixin[ + MetricRecord, ExportMetricsServiceRequest, MetricsExportResult + ], +): + # pylint: disable=unsubscriptable-object + """OTLP metrics exporter + + Args: + endpoint: OpenTelemetry Collector receiver endpoint + credentials: Credentials object for server authentication + metadata: Metadata to send when exporting + """ + + _stub = MetricsServiceStub + _result = MetricsExportResult + + # pylint: disable=no-self-use + def _translate_data( + self, data: Sequence[MetricRecord] + ) -> ExportMetricsServiceRequest: + # pylint: disable=too-many-locals,no-member + # pylint: disable=attribute-defined-outside-init + + sdk_resource_instrumentation_library_metrics = {} + + # The criteria to decide how to translate data is based on this table + # taken directly from OpenTelemetry Proto v0.5.0: + + # TODO: Update table after the decision on: + # https://github.com/open-telemetry/opentelemetry-specification/issues/731. + # By default, metrics recording using the OpenTelemetry API are exported as + # (the table does not include MeasurementValueType to avoid extra rows): + # + # Instrument Type + # ---------------------------------------------- + # Counter Sum(aggregation_temporality=delta;is_monotonic=true) + # UpDownCounter Sum(aggregation_temporality=delta;is_monotonic=false) + # ValueRecorder TBD + # SumObserver Sum(aggregation_temporality=cumulative;is_monotonic=true) + # UpDownSumObserver Sum(aggregation_temporality=cumulative;is_monotonic=false) + # ValueObserver Gauge() + for sdk_metric in data: + + if sdk_metric.resource not in ( + sdk_resource_instrumentation_library_metrics.keys() + ): + sdk_resource_instrumentation_library_metrics[ + sdk_metric.resource + ] = InstrumentationLibraryMetrics() + + type_class = { + int: { + "sum": {"class": IntSum, "argument": "int_sum"}, + "gauge": {"class": IntGauge, "argument": "int_gauge"}, + "data_point_class": IntDataPoint, + }, + float: { + "sum": {"class": DoubleSum, "argument": "double_sum"}, + "gauge": { + "class": DoubleGauge, + "argument": "double_gauge", + }, + "data_point_class": DoubleDataPoint, + }, + } + + value_type = sdk_metric.instrument.value_type + + sum_class = type_class[value_type]["sum"]["class"] + gauge_class = type_class[value_type]["gauge"]["class"] + data_point_class = type_class[value_type]["data_point_class"] + + if isinstance(sdk_metric.instrument, Counter): + otlp_metric_data = sum_class( + data_points=_get_data_points(sdk_metric, data_point_class), + aggregation_temporality=( + AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA + ), + is_monotonic=True, + ) + argument = type_class[value_type]["sum"]["argument"] + + elif isinstance(sdk_metric.instrument, UpDownCounter): + otlp_metric_data = sum_class( + data_points=_get_data_points(sdk_metric, data_point_class), + aggregation_temporality=( + AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA + ), + is_monotonic=False, + ) + argument = type_class[value_type]["sum"]["argument"] + + elif isinstance(sdk_metric.instrument, (ValueRecorder)): + logger.warning("Skipping exporting of ValueRecorder metric") + continue + + elif isinstance(sdk_metric.instrument, SumObserver): + otlp_metric_data = sum_class( + data_points=_get_data_points(sdk_metric, data_point_class), + aggregation_temporality=( + AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE + ), + is_monotonic=True, + ) + argument = type_class[value_type]["sum"]["argument"] + + elif isinstance(sdk_metric.instrument, UpDownSumObserver): + otlp_metric_data = sum_class( + data_points=_get_data_points(sdk_metric, data_point_class), + aggregation_temporality=( + AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE + ), + is_monotonic=False, + ) + argument = type_class[value_type]["sum"]["argument"] + + elif isinstance(sdk_metric.instrument, (ValueObserver)): + otlp_metric_data = gauge_class( + data_points=_get_data_points(sdk_metric, data_point_class) + ) + argument = type_class[value_type]["gauge"]["argument"] + + sdk_resource_instrumentation_library_metrics[ + sdk_metric.resource + ].metrics.append( + OTLPMetric( + **{ + "name": sdk_metric.instrument.name, + "description": sdk_metric.instrument.description, + "unit": sdk_metric.instrument.unit, + argument: otlp_metric_data, + } + ) + ) + + return ExportMetricsServiceRequest( + resource_metrics=_get_resource_data( + sdk_resource_instrumentation_library_metrics, + ResourceMetrics, + "metrics", + ) + ) + + def export(self, metrics: Sequence[MetricRecord]) -> MetricsExportResult: + # pylint: disable=arguments-differ + return self._export(metrics) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py new file mode 100644 index 0000000000..e518716d39 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -0,0 +1,228 @@ +# 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. + +"""OTLP Span Exporter""" + +import logging +from typing import Sequence + +from opentelemetry.exporter.otlp.exporter import ( + OTLPExporterMixin, + _get_resource_data, + _translate_key_values, +) +from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( + ExportTraceServiceRequest, +) +from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( + TraceServiceStub, +) +from opentelemetry.proto.common.v1.common_pb2 import InstrumentationLibrary +from opentelemetry.proto.trace.v1.trace_pb2 import ( + InstrumentationLibrarySpans, + ResourceSpans, +) +from opentelemetry.proto.trace.v1.trace_pb2 import Span as CollectorSpan +from opentelemetry.proto.trace.v1.trace_pb2 import Status +from opentelemetry.sdk.trace import Span as SDKSpan +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult + +logger = logging.getLogger(__name__) + + +# pylint: disable=no-member +class OTLPSpanExporter( + SpanExporter, + OTLPExporterMixin[SDKSpan, ExportTraceServiceRequest, SpanExportResult], +): + # pylint: disable=unsubscriptable-object + """OTLP span exporter + + Args: + endpoint: OpenTelemetry Collector receiver endpoint + credentials: Credentials object for server authentication + metadata: Metadata to send when exporting + """ + + _result = SpanExportResult + _stub = TraceServiceStub + + def _translate_name(self, sdk_span: SDKSpan) -> None: + self._collector_span_kwargs["name"] = sdk_span.name + + def _translate_start_time(self, sdk_span: SDKSpan) -> None: + self._collector_span_kwargs[ + "start_time_unix_nano" + ] = sdk_span.start_time + + def _translate_end_time(self, sdk_span: SDKSpan) -> None: + self._collector_span_kwargs["end_time_unix_nano"] = sdk_span.end_time + + def _translate_span_id(self, sdk_span: SDKSpan) -> None: + self._collector_span_kwargs[ + "span_id" + ] = sdk_span.context.span_id.to_bytes(8, "big") + + def _translate_trace_id(self, sdk_span: SDKSpan) -> None: + self._collector_span_kwargs[ + "trace_id" + ] = sdk_span.context.trace_id.to_bytes(16, "big") + + def _translate_parent(self, sdk_span: SDKSpan) -> None: + if sdk_span.parent is not None: + self._collector_span_kwargs[ + "parent_span_id" + ] = sdk_span.parent.span_id.to_bytes(8, "big") + + def _translate_context_trace_state(self, sdk_span: SDKSpan) -> None: + if sdk_span.context.trace_state is not None: + self._collector_span_kwargs["trace_state"] = ",".join( + [ + "{}={}".format(key, value) + for key, value in (sdk_span.context.trace_state.items()) + ] + ) + + def _translate_attributes(self, sdk_span: SDKSpan) -> None: + if sdk_span.attributes: + + self._collector_span_kwargs["attributes"] = [] + + for key, value in sdk_span.attributes.items(): + + try: + self._collector_span_kwargs["attributes"].append( + _translate_key_values(key, value) + ) + except Exception as error: # pylint: disable=broad-except + logger.exception(error) + + def _translate_events(self, sdk_span: SDKSpan) -> None: + if sdk_span.events: + self._collector_span_kwargs["events"] = [] + + for sdk_span_event in sdk_span.events: + + collector_span_event = CollectorSpan.Event( + name=sdk_span_event.name, + time_unix_nano=sdk_span_event.timestamp, + ) + + for key, value in sdk_span_event.attributes.items(): + try: + collector_span_event.attributes.append( + _translate_key_values(key, value) + ) + # pylint: disable=broad-except + except Exception as error: + logger.exception(error) + + self._collector_span_kwargs["events"].append( + collector_span_event + ) + + def _translate_links(self, sdk_span: SDKSpan) -> None: + if sdk_span.links: + self._collector_span_kwargs["links"] = [] + + for sdk_span_link in sdk_span.links: + + collector_span_link = CollectorSpan.Link( + trace_id=( + sdk_span_link.context.trace_id.to_bytes(16, "big") + ), + span_id=(sdk_span_link.context.span_id.to_bytes(8, "big")), + ) + + for key, value in sdk_span_link.attributes.items(): + try: + collector_span_link.attributes.append( + _translate_key_values(key, value) + ) + # pylint: disable=broad-except + except Exception as error: + logger.exception(error) + + self._collector_span_kwargs["links"].append( + collector_span_link + ) + + def _translate_status(self, sdk_span: SDKSpan) -> None: + if sdk_span.status is not None: + self._collector_span_kwargs["status"] = Status( + code=sdk_span.status.canonical_code.value, + message=sdk_span.status.description, + ) + + def _translate_data( + self, data: Sequence[SDKSpan] + ) -> ExportTraceServiceRequest: + # pylint: disable=attribute-defined-outside-init + + sdk_resource_instrumentation_library_spans = {} + + for sdk_span in data: + + if sdk_span.resource not in ( + sdk_resource_instrumentation_library_spans.keys() + ): + if sdk_span.instrumentation_info is not None: + instrumentation_library_spans = InstrumentationLibrarySpans( + instrumentation_library=InstrumentationLibrary( + name=sdk_span.instrumentation_info.name, + version=sdk_span.instrumentation_info.version, + ) + ) + + else: + instrumentation_library_spans = ( + InstrumentationLibrarySpans() + ) + + sdk_resource_instrumentation_library_spans[ + sdk_span.resource + ] = instrumentation_library_spans + + self._collector_span_kwargs = {} + + self._translate_name(sdk_span) + self._translate_start_time(sdk_span) + self._translate_end_time(sdk_span) + self._translate_span_id(sdk_span) + self._translate_trace_id(sdk_span) + self._translate_parent(sdk_span) + self._translate_context_trace_state(sdk_span) + self._translate_attributes(sdk_span) + self._translate_events(sdk_span) + self._translate_links(sdk_span) + self._translate_status(sdk_span) + + self._collector_span_kwargs["kind"] = getattr( + CollectorSpan.SpanKind, + "SPAN_KIND_{}".format(sdk_span.kind.name), + ) + + sdk_resource_instrumentation_library_spans[ + sdk_span.resource + ].spans.append(CollectorSpan(**self._collector_span_kwargs)) + + return ExportTraceServiceRequest( + resource_spans=_get_resource_data( + sdk_resource_instrumentation_library_spans, + ResourceSpans, + "spans", + ) + ) + + def export(self, spans: Sequence[SDKSpan]) -> SpanExportResult: + return self._export(spans) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py new file mode 100644 index 0000000000..e7b342d644 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/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.15.dev0" diff --git a/exporter/opentelemetry-exporter-otlp/tests/__init__.py b/exporter/opentelemetry-exporter-otlp/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py new file mode 100644 index 0000000000..21a718b84e --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -0,0 +1,117 @@ +# 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 collections import OrderedDict +from unittest import TestCase +from unittest.mock import patch + +from opentelemetry.exporter.otlp.metrics_exporter import OTLPMetricsExporter +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, +) +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue, + KeyValue, + StringKeyValue, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + AggregationTemporality, + InstrumentationLibraryMetrics, + IntDataPoint, + IntSum, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import Metric as OTLPMetric +from opentelemetry.proto.metrics.v1.metrics_pb2 import ResourceMetrics +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as OTLPResource, +) +from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export import MetricRecord +from opentelemetry.sdk.metrics.export.aggregate import SumAggregator +from opentelemetry.sdk.resources import Resource as SDKResource + + +class TestOTLPMetricExporter(TestCase): + def setUp(self): + self.exporter = OTLPMetricsExporter() + resource = SDKResource(OrderedDict([("a", 1), ("b", False)])) + self.counter_metric_record = MetricRecord( + Counter( + "a", + "b", + "c", + int, + MeterProvider(resource=resource,).get_meter(__name__), + ("d",), + ), + OrderedDict([("e", "f")]), + SumAggregator(), + resource, + ) + + @patch("opentelemetry.sdk.metrics.export.aggregate.time_ns") + def test_translate_metrics(self, mock_time_ns): + # pylint: disable=no-member + + mock_time_ns.configure_mock(**{"return_value": 1}) + + self.counter_metric_record.instrument.add(1, OrderedDict([("a", "b")])) + + expected = ExportMetricsServiceRequest( + resource_metrics=[ + ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + instrumentation_library_metrics=[ + InstrumentationLibraryMetrics( + metrics=[ + OTLPMetric( + name="a", + description="b", + unit="c", + int_sum=IntSum( + data_points=[ + IntDataPoint( + labels=[ + StringKeyValue( + key="a", value="b" + ) + ], + value=1, + time_unix_nano=1, + ) + ], + aggregation_temporality=( + AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA + ), + is_monotonic=True, + ), + ) + ] + ) + ], + ) + ] + ) + + # pylint: disable=protected-access + actual = self.exporter._translate_data([self.counter_metric_record]) + + self.assertEqual(expected, actual) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py new file mode 100644 index 0000000000..e8c449c9df --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -0,0 +1,305 @@ +# 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 collections import OrderedDict +from concurrent.futures import ThreadPoolExecutor +from unittest import TestCase +from unittest.mock import Mock, PropertyMock, patch + +from google.protobuf.duration_pb2 import Duration +from google.rpc.error_details_pb2 import RetryInfo +from grpc import StatusCode, server + +from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter +from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( + ExportTraceServiceRequest, + ExportTraceServiceResponse, +) +from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( + TraceServiceServicer, + add_TraceServiceServicer_to_server, +) +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue, + InstrumentationLibrary, + KeyValue, +) +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as OTLPResource, +) +from opentelemetry.proto.trace.v1.trace_pb2 import ( + InstrumentationLibrarySpans, + ResourceSpans, +) +from opentelemetry.proto.trace.v1.trace_pb2 import Span as OTLPSpan +from opentelemetry.proto.trace.v1.trace_pb2 import Status +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.trace import TracerProvider, _Span +from opentelemetry.sdk.trace.export import ( + SimpleExportSpanProcessor, + SpanExportResult, +) +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo + + +class TraceServiceServicerUNAVAILABLEDelay(TraceServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.UNAVAILABLE) + + context.send_initial_metadata( + (("google.rpc.retryinfo-bin", RetryInfo().SerializeToString()),) + ) + context.set_trailing_metadata( + ( + ( + "google.rpc.retryinfo-bin", + RetryInfo( + retry_delay=Duration(seconds=4) + ).SerializeToString(), + ), + ) + ) + + return ExportTraceServiceResponse() + + +class TraceServiceServicerUNAVAILABLE(TraceServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.UNAVAILABLE) + + return ExportTraceServiceResponse() + + +class TraceServiceServicerSUCCESS(TraceServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.OK) + + return ExportTraceServiceResponse() + + +class TraceServiceServicerALREADY_EXISTS(TraceServiceServicer): + # pylint: disable=invalid-name,unused-argument,no-self-use + def Export(self, request, context): + context.set_code(StatusCode.ALREADY_EXISTS) + + return ExportTraceServiceResponse() + + +class TestOTLPSpanExporter(TestCase): + def setUp(self): + tracer_provider = TracerProvider() + self.exporter = OTLPSpanExporter() + tracer_provider.add_span_processor( + SimpleExportSpanProcessor(self.exporter) + ) + self.tracer = tracer_provider.get_tracer(__name__) + + self.server = server(ThreadPoolExecutor(max_workers=10)) + + self.server.add_insecure_port("[::]:55680") + + self.server.start() + + event_mock = Mock( + **{ + "timestamp": 1591240820506462784, + "attributes": OrderedDict([("a", 1), ("b", False)]), + } + ) + + type(event_mock).name = PropertyMock(return_value="a") + + self.span = _Span( + "a", + context=Mock( + **{ + "trace_state": OrderedDict([("a", "b"), ("c", "d")]), + "span_id": 10217189687419569865, + "trace_id": 67545097771067222548457157018666467027, + } + ), + resource=SDKResource(OrderedDict([("a", 1), ("b", False)])), + parent=Mock(**{"span_id": 12345}), + attributes=OrderedDict([("a", 1), ("b", True)]), + events=[event_mock], + links=[ + Mock( + **{ + "context.trace_id": 1, + "context.span_id": 2, + "attributes": OrderedDict([("a", 1), ("b", False)]), + "kind": OTLPSpan.SpanKind.SPAN_KIND_INTERNAL, # pylint: disable=no-member + } + ) + ], + instrumentation_info=InstrumentationInfo( + name="name", version="version" + ), + ) + + self.span.start() + self.span.end() + + def tearDown(self): + self.server.stop(None) + + @patch("opentelemetry.exporter.otlp.exporter.expo") + @patch("opentelemetry.exporter.otlp.exporter.sleep") + def test_unavailable(self, mock_sleep, mock_expo): + + mock_expo.configure_mock(**{"return_value": [1]}) + + add_TraceServiceServicer_to_server( + TraceServiceServicerUNAVAILABLE(), self.server + ) + self.assertEqual( + self.exporter.export([self.span]), SpanExportResult.FAILURE + ) + mock_sleep.assert_called_with(1) + + @patch("opentelemetry.exporter.otlp.exporter.expo") + @patch("opentelemetry.exporter.otlp.exporter.sleep") + def test_unavailable_delay(self, mock_sleep, mock_expo): + + mock_expo.configure_mock(**{"return_value": [1]}) + + add_TraceServiceServicer_to_server( + TraceServiceServicerUNAVAILABLEDelay(), self.server + ) + self.assertEqual( + self.exporter.export([self.span]), SpanExportResult.FAILURE + ) + mock_sleep.assert_called_with(4) + + def test_success(self): + add_TraceServiceServicer_to_server( + TraceServiceServicerSUCCESS(), self.server + ) + self.assertEqual( + self.exporter.export([self.span]), SpanExportResult.SUCCESS + ) + + def test_failure(self): + add_TraceServiceServicer_to_server( + TraceServiceServicerALREADY_EXISTS(), self.server + ) + self.assertEqual( + self.exporter.export([self.span]), SpanExportResult.FAILURE + ) + + def test_translate_spans(self): + + expected = ExportTraceServiceRequest( + resource_spans=[ + ResourceSpans( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + instrumentation_library_spans=[ + InstrumentationLibrarySpans( + instrumentation_library=InstrumentationLibrary( + name="name", version="version" + ), + spans=[ + OTLPSpan( + # pylint: disable=no-member + name="a", + start_time_unix_nano=self.span.start_time, + end_time_unix_nano=self.span.end_time, + trace_state="a=b,c=d", + span_id=int.to_bytes( + 10217189687419569865, 8, "big" + ), + trace_id=int.to_bytes( + 67545097771067222548457157018666467027, + 16, + "big", + ), + parent_span_id=( + b"\000\000\000\000\000\00009" + ), + kind=( + OTLPSpan.SpanKind.SPAN_KIND_INTERNAL + ), + attributes=[ + KeyValue( + key="a", + value=AnyValue(int_value=1), + ), + KeyValue( + key="b", + value=AnyValue(bool_value=True), + ), + ], + events=[ + OTLPSpan.Event( + name="a", + time_unix_nano=1591240820506462784, + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=False + ), + ), + ], + ) + ], + status=Status(code=0, message=""), + links=[ + OTLPSpan.Link( + trace_id=int.to_bytes( + 1, 16, "big" + ), + span_id=int.to_bytes(2, 8, "big"), + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=False + ), + ), + ], + ) + ], + ) + ], + ) + ], + ), + ] + ) + + # pylint: disable=protected-access + self.assertEqual(expected, self.exporter._translate_data([self.span]))