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",
+ },
+ )