commit a49c812e6dc6b51c3d2a0a33b66d25b2046a7913 Author: Leighton Chen Date: Tue Aug 4 19:10:51 2020 -0700 Rename remaining framework packages from "ext" to "instrumentation" (#969) diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md new file mode 100644 index 000000000..5579a36a6 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## Unreleased + +- Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) +- Change package name to opentelemetry-instrumentation-elasticsearch + ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) + +## Version 0.10b0 + +Released 2020-06-23 + +- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/LICENSE b/instrumentation/opentelemetry-instrumentation-elasticsearch/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/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/instrumentation/opentelemetry-instrumentation-elasticsearch/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-elasticsearch/MANIFEST.in new file mode 100644 index 000000000..aed3e3327 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/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/instrumentation/opentelemetry-instrumentation-elasticsearch/README.rst b/instrumentation/opentelemetry-instrumentation-elasticsearch/README.rst new file mode 100644 index 000000000..9f898e783 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry elasticsearch Integration +======================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-elasticsearch.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-elasticsearch/ + +This library allows tracing elasticsearch made by the +`elasticsearch `_ library. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-elasticsearch + +References +---------- + +* `OpenTelemetry elasticsearch Integration `_ +* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg new file mode 100644 index 000000000..fc5f862a2 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.cfg @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-instrumentation-elasticsearch +description = OpenTelemetry elasticsearch instrumentation +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-elasticsearch +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.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 + wrapt >= 1.0.0, < 2.0.0 + elasticsearch >= 2.0 + +[options.extras_require] +test = + opentelemetry-test == 0.12.dev0 + elasticsearch-dsl >= 2.0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + elasticsearch = opentelemetry.instrumentation.elasticsearch:ElasticsearchInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.py new file mode 100644 index 000000000..cd7a7f101 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/setup.py @@ -0,0 +1,31 @@ +# 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", + "instrumentation", + "elasticsearch", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py new file mode 100644 index 000000000..f350a7dc2 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py @@ -0,0 +1,163 @@ +# 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 tracing HTTP elasticsearch made by the +`elasticsearch `_ library. + +Usage +----- + +.. code-block:: python + + from opentelemetry import trace + from opentelemetry.instrumentation.elasticsearch import ElasticSearchInstrumentor + from opentelemetry.sdk.trace import TracerProvider + import elasticsearch + + trace.set_tracer_provider(TracerProvider()) + + # instrument elasticsearch + ElasticSearchInstrumentor().instrument(tracer_provider=trace.get_tracer_provider()) + + # Using elasticsearch as normal now will automatically generate spans + es = elasticsearch.Elasticsearch() + es.index(index='my-index', doc_type='my-type', id=1, body={'my': 'data', 'timestamp': datetime.now()}) + es.get(index='my-index', doc_type='my-type', id=1) + +API +--- + +Elasticsearch instrumentation prefixes operation names with the string "Elasticsearch". This +can be changed to a different string by either setting the `OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX` +environment variable or by passing the prefix as an argument to the instrumentor. For example, + + +.. code-block:: python + + ElasticSearchInstrumentor("my-custom-prefix").instrument() +""" + +import functools +import types +from logging import getLogger +from os import environ + +import elasticsearch +import elasticsearch.exceptions +from wrapt import ObjectProxy +from wrapt import wrap_function_wrapper as _wrap + +from opentelemetry import context, propagators, trace +from opentelemetry.instrumentation.elasticsearch.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import unwrap +from opentelemetry.trace import SpanKind, get_tracer +from opentelemetry.trace.status import Status, StatusCanonicalCode + +logger = getLogger(__name__) + + +# Values to add as tags from the actual +# payload returned by Elasticsearch, if any. +_ATTRIBUTES_FROM_RESULT = [ + "found", + "timed_out", + "took", +] + +_DEFALT_OP_NAME = "request" + + +class ElasticsearchInstrumentor(BaseInstrumentor): + """An instrumentor for elasticsearch + See `BaseInstrumentor` + """ + + def __init__(self, span_name_prefix=None): + if not span_name_prefix: + span_name_prefix = environ.get( + "OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX", "Elasticsearch", + ) + self._span_name_prefix = span_name_prefix.strip() + super().__init__() + + def _instrument(self, **kwargs): + """ + Instruments elasticsarch module + """ + tracer_provider = kwargs.get("tracer_provider") + tracer = get_tracer(__name__, __version__, tracer_provider) + _wrap( + elasticsearch, + "Transport.perform_request", + _wrap_perform_request(tracer, self._span_name_prefix), + ) + + def _uninstrument(self, **kwargs): + unwrap(elasticsearch.Transport, "perform_request") + + +def _wrap_perform_request(tracer, span_name_prefix): + def wrapper(wrapped, _, args, kwargs): + method = url = None + try: + method, url, *_ = args + except IndexError: + logger.warning( + "expected perform_request to receive two positional arguments. " + "Got %d", + len(args), + ) + + op_name = span_name_prefix + (url or method or _DEFALT_OP_NAME) + params = kwargs.get("params", {}) + body = kwargs.get("body", None) + + attributes = { + "component": "elasticsearch-py", + "db.type": "elasticsearch", + } + + if url: + attributes["elasticsearch.url"] = url + if method: + attributes["elasticsearch.method"] = method + if body: + attributes["db.statement"] = str(body) + if params: + attributes["elasticsearch.params"] = str(params) + + with tracer.start_as_current_span( + op_name, kind=SpanKind.CLIENT, attributes=attributes + ) as span: + try: + rv = wrapped(*args, **kwargs) + if isinstance(rv, dict): + for member in _ATTRIBUTES_FROM_RESULT: + if member in rv: + span.set_attribute( + "elasticsearch.{0}".format(member), + str(rv[member]), + ) + return rv + except Exception as ex: # pylint: disable=broad-except + if isinstance(ex, elasticsearch.exceptions.NotFoundError): + status = StatusCanonicalCode.NOT_FOUND + else: + status = StatusCanonicalCode.UNKNOWN + span.set_status(Status(status, str(ex))) + raise ex + + return wrapper diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py new file mode 100644 index 000000000..780a92b6a --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/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.12.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es2.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es2.py new file mode 100644 index 000000000..008a95d67 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es2.py @@ -0,0 +1,33 @@ +from elasticsearch_dsl import ( # pylint: disable=no-name-in-module + DocType, + String, +) + + +class Article(DocType): + title = String(analyzer="snowball", fields={"raw": String()}) + body = String(analyzer="snowball") + + class Meta: + index = "test-index" + + +dsl_create_statement = { + "mappings": { + "article": { + "properties": { + "title": { + "analyzer": "snowball", + "fields": {"raw": {"type": "string"}}, + "type": "string", + }, + "body": {"analyzer": "snowball", "type": "string"}, + } + } + }, + "settings": {"analysis": {}}, +} +dsl_index_result = (1, {}, '{"created": true}') +dsl_index_span_name = "Elasticsearch/test-index/article/2" +dsl_index_url = "/test-index/article/2" +dsl_search_method = "GET" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es5.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es5.py new file mode 100644 index 000000000..cf32d9886 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es5.py @@ -0,0 +1,33 @@ +from elasticsearch_dsl import ( # pylint: disable=no-name-in-module + DocType, + Keyword, + Text, +) + + +class Article(DocType): + title = Text(analyzer="snowball", fields={"raw": Keyword()}) + body = Text(analyzer="snowball") + + class Meta: + index = "test-index" + + +dsl_create_statement = { + "mappings": { + "article": { + "properties": { + "title": { + "analyzer": "snowball", + "fields": {"raw": {"type": "keyword"}}, + "type": "text", + }, + "body": {"analyzer": "snowball", "type": "text"}, + } + } + }, +} +dsl_index_result = (1, {}, '{"created": true}') +dsl_index_span_name = "Elasticsearch/test-index/article/2" +dsl_index_url = "/test-index/article/2" +dsl_search_method = "GET" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py new file mode 100644 index 000000000..b27d291ba --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py @@ -0,0 +1,33 @@ +from elasticsearch_dsl import ( # pylint: disable=unused-import + Document, + Keyword, + Text, +) + + +class Article(Document): + title = Text(analyzer="snowball", fields={"raw": Keyword()}) + body = Text(analyzer="snowball") + + class Index: + name = "test-index" + + +dsl_create_statement = { + "mappings": { + "doc": { + "properties": { + "title": { + "analyzer": "snowball", + "fields": {"raw": {"type": "keyword"}}, + "type": "text", + }, + "body": {"analyzer": "snowball", "type": "text"}, + } + } + } +} +dsl_index_result = (1, {}, '{"result": "created"}') +dsl_index_span_name = "Elasticsearch/test-index/doc/2" +dsl_index_url = "/test-index/doc/2" +dsl_search_method = "GET" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py new file mode 100644 index 000000000..a2d37a54a --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py @@ -0,0 +1,31 @@ +from elasticsearch_dsl import ( # pylint: disable=unused-import + Document, + Keyword, + Text, +) + + +class Article(Document): + title = Text(analyzer="snowball", fields={"raw": Keyword()}) + body = Text(analyzer="snowball") + + class Index: + name = "test-index" + + +dsl_create_statement = { + "mappings": { + "properties": { + "title": { + "analyzer": "snowball", + "fields": {"raw": {"type": "keyword"}}, + "type": "text", + }, + "body": {"analyzer": "snowball", "type": "text"}, + } + } +} +dsl_index_result = (1, {}, '{"result": "created"}') +dsl_index_span_name = "Elasticsearch/test-index/_doc/2" +dsl_index_url = "/test-index/_doc/2" +dsl_search_method = "POST" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py new file mode 100644 index 000000000..cc1d31477 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py @@ -0,0 +1,309 @@ +# 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 threading +from ast import literal_eval +from unittest import mock + +import elasticsearch +import elasticsearch.exceptions +from elasticsearch import Elasticsearch +from elasticsearch_dsl import Search + +import opentelemetry.instrumentation.elasticsearch +from opentelemetry.instrumentation.elasticsearch import ( + ElasticsearchInstrumentor, +) +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace.status import StatusCanonicalCode + +major_version = elasticsearch.VERSION[0] + +if major_version == 7: + from . import helpers_es7 as helpers # pylint: disable=no-name-in-module +elif major_version == 6: + from . import helpers_es6 as helpers # pylint: disable=no-name-in-module +elif major_version == 5: + from . import helpers_es5 as helpers # pylint: disable=no-name-in-module +else: + from . import helpers_es2 as helpers # pylint: disable=no-name-in-module + + +Article = helpers.Article + + +@mock.patch( + "elasticsearch.connection.http_urllib3.Urllib3HttpConnection.perform_request" +) +class TestElasticsearchIntegration(TestBase): + def setUp(self): + super().setUp() + self.tracer = self.tracer_provider.get_tracer(__name__) + ElasticsearchInstrumentor().instrument() + + def tearDown(self): + super().tearDown() + with self.disable_logging(): + ElasticsearchInstrumentor().uninstrument() + + def get_ordered_finished_spans(self): + return sorted( + self.memory_exporter.get_finished_spans(), + key=lambda s: s.start_time, + ) + + def test_instrumentor(self, request_mock): + request_mock.return_value = (1, {}, {}) + + es = Elasticsearch() + es.index(index="sw", doc_type="people", id=1, body={"name": "adam"}) + + spans_list = self.get_ordered_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + # Check version and name in span's instrumentation info + # self.check_span_instrumentation_info(span, opentelemetry.instrumentation.elasticsearch) + self.check_span_instrumentation_info( + span, opentelemetry.instrumentation.elasticsearch + ) + + # check that no spans are generated after uninstrument + ElasticsearchInstrumentor().uninstrument() + + es.index(index="sw", doc_type="people", id=1, body={"name": "adam"}) + + spans_list = self.get_ordered_finished_spans() + self.assertEqual(len(spans_list), 1) + + def test_prefix_arg(self, request_mock): + prefix = "prefix-from-env" + ElasticsearchInstrumentor().uninstrument() + ElasticsearchInstrumentor(span_name_prefix=prefix).instrument() + request_mock.return_value = (1, {}, {}) + self._test_prefix(prefix) + + def test_prefix_env(self, request_mock): + prefix = "prefix-from-args" + env_var = "OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX" + os.environ[env_var] = prefix + ElasticsearchInstrumentor().uninstrument() + ElasticsearchInstrumentor().instrument() + request_mock.return_value = (1, {}, {}) + del os.environ[env_var] + self._test_prefix(prefix) + + def _test_prefix(self, prefix): + es = Elasticsearch() + es.index(index="sw", doc_type="people", id=1, body={"name": "adam"}) + + spans_list = self.get_ordered_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertTrue(span.name.startswith(prefix)) + + def test_result_values(self, request_mock): + request_mock.return_value = ( + 1, + {}, + '{"found": false, "timed_out": true, "took": 7}', + ) + es = Elasticsearch() + es.get(index="test-index", doc_type="tweet", id=1) + + spans = self.get_ordered_finished_spans() + + self.assertEqual(1, len(spans)) + self.assertEqual("False", spans[0].attributes["elasticsearch.found"]) + self.assertEqual( + "True", spans[0].attributes["elasticsearch.timed_out"] + ) + self.assertEqual("7", spans[0].attributes["elasticsearch.took"]) + + def test_trace_error_unknown(self, request_mock): + exc = RuntimeError("custom error") + request_mock.side_effect = exc + self._test_trace_error(StatusCanonicalCode.UNKNOWN, exc) + + def test_trace_error_not_found(self, request_mock): + msg = "record not found" + exc = elasticsearch.exceptions.NotFoundError(404, msg) + request_mock.return_value = (1, {}, {}) + request_mock.side_effect = exc + self._test_trace_error(StatusCanonicalCode.NOT_FOUND, exc) + + def _test_trace_error(self, code, exc): + es = Elasticsearch() + try: + es.get(index="test-index", doc_type="tweet", id=1) + except Exception: # pylint: disable=broad-except + pass + + spans = self.get_ordered_finished_spans() + self.assertEqual(1, len(spans)) + span = spans[0] + self.assertFalse(span.status.is_ok) + self.assertEqual(span.status.canonical_code, code) + self.assertEqual(span.status.description, str(exc)) + + def test_parent(self, request_mock): + request_mock.return_value = (1, {}, {}) + es = Elasticsearch() + with self.tracer.start_as_current_span("parent"): + es.index( + index="sw", doc_type="people", id=1, body={"name": "adam"} + ) + + spans = self.get_ordered_finished_spans() + self.assertEqual(len(spans), 2) + + self.assertEqual(spans[0].name, "parent") + self.assertEqual(spans[1].name, "Elasticsearch/sw/people/1") + self.assertIsNotNone(spans[1].parent) + self.assertEqual(spans[1].parent.span_id, spans[0].context.span_id) + + def test_multithread(self, request_mock): + request_mock.return_value = (1, {}, {}) + es = Elasticsearch() + ev = threading.Event() + + # 1. Start tracing from thread-1; make thread-2 wait + # 2. Trace something from thread-2, make thread-1 join before finishing. + # 3. Check the spans got different parents, and are in the expected order. + def target1(parent_span): + with self.tracer.use_span(parent_span): + es.get(index="test-index", doc_type="tweet", id=1) + ev.set() + ev.wait() + + def target2(): + ev.wait() + es.get(index="test-index", doc_type="tweet", id=2) + ev.set() + + with self.tracer.start_as_current_span("parent") as span: + t1 = threading.Thread(target=target1, args=(span,)) + t1.start() + + t2 = threading.Thread(target=target2) + t2.start() + t1.join() + t2.join() + + spans = self.get_ordered_finished_spans() + self.assertEqual(3, len(spans)) + s1, s2, s3 = spans + + self.assertEqual(s1.name, "parent") + + self.assertEqual(s2.name, "Elasticsearch/test-index/tweet/1") + self.assertIsNotNone(s2.parent) + self.assertEqual(s2.parent.span_id, s1.context.span_id) + self.assertEqual(s3.name, "Elasticsearch/test-index/tweet/2") + self.assertIsNone(s3.parent) + + def test_dsl_search(self, request_mock): + request_mock.return_value = (1, {}, '{"hits": {"hits": []}}') + + client = Elasticsearch() + search = Search(using=client, index="test-index").filter( + "term", author="testing" + ) + search.execute() + spans = self.get_ordered_finished_spans() + span = spans[0] + self.assertEqual(1, len(spans)) + self.assertEqual(span.name, "Elasticsearch/test-index/_search") + self.assertIsNotNone(span.end_time) + self.assertEqual( + span.attributes, + { + "component": "elasticsearch-py", + "db.type": "elasticsearch", + "elasticsearch.url": "/test-index/_search", + "elasticsearch.method": helpers.dsl_search_method, + "db.statement": str( + { + "query": { + "bool": { + "filter": [{"term": {"author": "testing"}}] + } + } + } + ), + }, + ) + + def test_dsl_create(self, request_mock): + request_mock.return_value = (1, {}, {}) + client = Elasticsearch() + Article.init(using=client) + + spans = self.get_ordered_finished_spans() + self.assertEqual(2, len(spans)) + span1, span2 = spans + self.assertEqual(span1.name, "Elasticsearch/test-index") + self.assertEqual( + span1.attributes, + { + "component": "elasticsearch-py", + "db.type": "elasticsearch", + "elasticsearch.url": "/test-index", + "elasticsearch.method": "HEAD", + }, + ) + + self.assertEqual(span2.name, "Elasticsearch/test-index") + attributes = { + "component": "elasticsearch-py", + "db.type": "elasticsearch", + "elasticsearch.url": "/test-index", + "elasticsearch.method": "PUT", + } + self.assert_span_has_attributes(span2, attributes) + self.assertEqual( + literal_eval(span2.attributes["db.statement"]), + helpers.dsl_create_statement, + ) + + def test_dsl_index(self, request_mock): + request_mock.return_value = helpers.dsl_index_result + + client = Elasticsearch() + article = Article( + meta={"id": 2}, + title="About searching", + body="A few words here, a few words there", + ) + res = article.save(using=client) + self.assertTrue(res) + spans = self.get_ordered_finished_spans() + self.assertEqual(1, len(spans)) + span = spans[0] + self.assertEqual(span.name, helpers.dsl_index_span_name) + attributes = { + "component": "elasticsearch-py", + "db.type": "elasticsearch", + "elasticsearch.url": helpers.dsl_index_url, + "elasticsearch.method": "PUT", + } + self.assert_span_has_attributes(span, attributes) + self.assertEqual( + literal_eval(span.attributes["db.statement"]), + { + "body": "A few words here, a few words there", + "title": "About searching", + }, + )