We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
elasticsearch instrumentation creates too many span names, with unique document IDs.
**Describe your environment**
Using `opentelemetry-instrumentation-elasticsearch` `0.24b0` with `elasticsearch` `7.14.1`

**Steps to reproduce**
Perform some `index()` and/or `delete()` calls with `id` parameter set

**What is the expected behavior?**
The number of span names created is reasonable and finite

**What is the actual behavior?**
An unbounded number of span names get created, containing the unique document names. This makes it hard to search through operations, for example in Jaeger:

![screenshot](https://user-images.githubusercontent.com/426784/135645077-dde57682-5398-4e47-92d3-bf6e0eddb5ea.png)

The `datamart.test.xxxx` parts are the Elasticsearch document IDs.

**Additional context**
I have a patch for this incoming.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py`
Content:
```
1 # Copyright The OpenTelemetry Authors
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 
15 """
16 This library allows tracing HTTP elasticsearch made by the
17 `elasticsearch <https://elasticsearch-py.readthedocs.io/en/master/>`_ library.
18 
19 Usage
20 -----
21 
22 .. code-block:: python
23 
24     from opentelemetry.instrumentation.elasticsearch import ElasticsearchInstrumentor
25     import elasticsearch
26 
27 
28     # instrument elasticsearch
29     ElasticsearchInstrumentor().instrument()
30 
31     # Using elasticsearch as normal now will automatically generate spans
32     es = elasticsearch.Elasticsearch()
33     es.index(index='my-index', doc_type='my-type', id=1, body={'my': 'data', 'timestamp': datetime.now()})
34     es.get(index='my-index', doc_type='my-type', id=1)
35 
36 API
37 ---
38 
39 Elasticsearch instrumentation prefixes operation names with the string "Elasticsearch". This
40 can be changed to a different string by either setting the `OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX`
41 environment variable or by passing the prefix as an argument to the instrumentor. For example,
42 
43 
44 .. code-block:: python
45 
46     ElasticsearchInstrumentor("my-custom-prefix").instrument()
47 
48 
49 The `instrument` method accepts the following keyword args:
50 
51 tracer_provider (TracerProvider) - an optional tracer provider
52 request_hook (Callable) - a function with extra user-defined logic to be performed before performing the request
53                           this function signature is:
54                           def request_hook(span: Span, method: str, url: str, kwargs)
55 response_hook (Callable) - a function with extra user-defined logic to be performed after performing the request
56                           this function signature is:
57                           def response_hook(span: Span, response: dict)
58 
59 for example:
60 
61 .. code: python
62 
63     from opentelemetry.instrumentation.elasticsearch import ElasticsearchInstrumentor
64     import elasticsearch
65 
66     def request_hook(span, method, url, kwargs):
67         if span and span.is_recording():
68             span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
69 
70     def response_hook(span, response):
71         if span and span.is_recording():
72             span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
73 
74     # instrument elasticsearch with request and response hooks
75     ElasticsearchInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook)
76 
77     # Using elasticsearch as normal now will automatically generate spans,
78     # including user custom attributes added from the hooks
79     es = elasticsearch.Elasticsearch()
80     es.index(index='my-index', doc_type='my-type', id=1, body={'my': 'data', 'timestamp': datetime.now()})
81     es.get(index='my-index', doc_type='my-type', id=1)
82 """
83 
84 from logging import getLogger
85 from os import environ
86 from typing import Collection
87 
88 import elasticsearch
89 import elasticsearch.exceptions
90 from wrapt import wrap_function_wrapper as _wrap
91 
92 from opentelemetry.instrumentation.elasticsearch.package import _instruments
93 from opentelemetry.instrumentation.elasticsearch.version import __version__
94 from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
95 from opentelemetry.instrumentation.utils import unwrap
96 from opentelemetry.semconv.trace import SpanAttributes
97 from opentelemetry.trace import SpanKind, get_tracer
98 
99 logger = getLogger(__name__)
100 
101 
102 # Values to add as tags from the actual
103 # payload returned by Elasticsearch, if any.
104 _ATTRIBUTES_FROM_RESULT = [
105     "found",
106     "timed_out",
107     "took",
108 ]
109 
110 _DEFALT_OP_NAME = "request"
111 
112 
113 class ElasticsearchInstrumentor(BaseInstrumentor):
114     """An instrumentor for elasticsearch
115     See `BaseInstrumentor`
116     """
117 
118     def __init__(self, span_name_prefix=None):
119         if not span_name_prefix:
120             span_name_prefix = environ.get(
121                 "OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX", "Elasticsearch",
122             )
123         self._span_name_prefix = span_name_prefix.strip()
124         super().__init__()
125 
126     def instrumentation_dependencies(self) -> Collection[str]:
127         return _instruments
128 
129     def _instrument(self, **kwargs):
130         """
131         Instruments elasticsarch module
132         """
133         tracer_provider = kwargs.get("tracer_provider")
134         tracer = get_tracer(__name__, __version__, tracer_provider)
135         request_hook = kwargs.get("request_hook")
136         response_hook = kwargs.get("response_hook")
137         _wrap(
138             elasticsearch,
139             "Transport.perform_request",
140             _wrap_perform_request(
141                 tracer, self._span_name_prefix, request_hook, response_hook
142             ),
143         )
144 
145     def _uninstrument(self, **kwargs):
146         unwrap(elasticsearch.Transport, "perform_request")
147 
148 
149 def _wrap_perform_request(
150     tracer, span_name_prefix, request_hook=None, response_hook=None
151 ):
152     # pylint: disable=R0912
153     def wrapper(wrapped, _, args, kwargs):
154         method = url = None
155         try:
156             method, url, *_ = args
157         except IndexError:
158             logger.warning(
159                 "expected perform_request to receive two positional arguments. "
160                 "Got %d",
161                 len(args),
162             )
163 
164         op_name = span_name_prefix + (url or method or _DEFALT_OP_NAME)
165         params = kwargs.get("params", {})
166         body = kwargs.get("body", None)
167 
168         with tracer.start_as_current_span(
169             op_name, kind=SpanKind.CLIENT,
170         ) as span:
171 
172             if callable(request_hook):
173                 request_hook(span, method, url, kwargs)
174 
175             if span.is_recording():
176                 attributes = {
177                     SpanAttributes.DB_SYSTEM: "elasticsearch",
178                 }
179                 if url:
180                     attributes["elasticsearch.url"] = url
181                 if method:
182                     attributes["elasticsearch.method"] = method
183                 if body:
184                     attributes[SpanAttributes.DB_STATEMENT] = str(body)
185                 if params:
186                     attributes["elasticsearch.params"] = str(params)
187                 for key, value in attributes.items():
188                     span.set_attribute(key, value)
189 
190             rv = wrapped(*args, **kwargs)
191             if isinstance(rv, dict) and span.is_recording():
192                 for member in _ATTRIBUTES_FROM_RESULT:
193                     if member in rv:
194                         span.set_attribute(
195                             f"elasticsearch.{member}", str(rv[member]),
196                         )
197 
198             if callable(response_hook):
199                 response_hook(span, rv)
200             return rv
201 
202     return wrapper
203
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py
--- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py
@@ -81,6 +81,7 @@
     es.get(index='my-index', doc_type='my-type', id=1)
 """
 
+import re
 from logging import getLogger
 from os import environ
 from typing import Collection
@@ -107,7 +108,7 @@
     "took",
 ]
 
-_DEFALT_OP_NAME = "request"
+_DEFAULT_OP_NAME = "request"
 
 
 class ElasticsearchInstrumentor(BaseInstrumentor):
@@ -146,6 +147,9 @@
         unwrap(elasticsearch.Transport, "perform_request")
 
 
+_regex_doc_url = re.compile(r"/_doc/([^/]+)")
+
+
 def _wrap_perform_request(
     tracer, span_name_prefix, request_hook=None, response_hook=None
 ):
@@ -161,7 +165,24 @@
                 len(args),
             )
 
-        op_name = span_name_prefix + (url or method or _DEFALT_OP_NAME)
+        op_name = span_name_prefix + (url or method or _DEFAULT_OP_NAME)
+        doc_id = None
+        if url:
+            # TODO: This regex-based solution avoids creating an unbounded number of span names, but should be replaced by instrumenting individual Elasticsearch methods instead of Transport.perform_request()
+            # A limitation of the regex is that only the '_doc' mapping type is supported. Mapping types are deprecated since Elasticsearch 7
+            # https://github.com/open-telemetry/opentelemetry-python-contrib/issues/708
+            match = _regex_doc_url.search(url)
+            if match is not None:
+                # Remove the full document ID from the URL
+                doc_span = match.span()
+                op_name = (
+                    span_name_prefix
+                    + url[: doc_span[0]]
+                    + "/_doc/:id"
+                    + url[doc_span[1] :]
+                )
+                # Put the document ID in attributes
+                doc_id = match.group(1)
         params = kwargs.get("params", {})
         body = kwargs.get("body", None)
 
@@ -184,6 +205,8 @@
                     attributes[SpanAttributes.DB_STATEMENT] = str(body)
                 if params:
                     attributes["elasticsearch.params"] = str(params)
+                if doc_id:
+                    attributes["elasticsearch.id"] = doc_id
                 for key, value in attributes.items():
                     span.set_attribute(key, value)

{"golden_diff": "diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py\n--- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py\n+++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py\n@@ -81,6 +81,7 @@\n     es.get(index='my-index', doc_type='my-type', id=1)\n \"\"\"\n \n+import re\n from logging import getLogger\n from os import environ\n from typing import Collection\n@@ -107,7 +108,7 @@\n     \"took\",\n ]\n \n-_DEFALT_OP_NAME = \"request\"\n+_DEFAULT_OP_NAME = \"request\"\n \n \n class ElasticsearchInstrumentor(BaseInstrumentor):\n@@ -146,6 +147,9 @@\n         unwrap(elasticsearch.Transport, \"perform_request\")\n \n \n+_regex_doc_url = re.compile(r\"/_doc/([^/]+)\")\n+\n+\n def _wrap_perform_request(\n     tracer, span_name_prefix, request_hook=None, response_hook=None\n ):\n@@ -161,7 +165,24 @@\n                 len(args),\n             )\n \n-        op_name = span_name_prefix + (url or method or _DEFALT_OP_NAME)\n+        op_name = span_name_prefix + (url or method or _DEFAULT_OP_NAME)\n+        doc_id = None\n+        if url:\n+            # TODO: This regex-based solution avoids creating an unbounded number of span names, but should be replaced by instrumenting individual Elasticsearch methods instead of Transport.perform_request()\n+            # A limitation of the regex is that only the '_doc' mapping type is supported. Mapping types are deprecated since Elasticsearch 7\n+            # https://github.com/open-telemetry/opentelemetry-python-contrib/issues/708\n+            match = _regex_doc_url.search(url)\n+            if match is not None:\n+                # Remove the full document ID from the URL\n+                doc_span = match.span()\n+                op_name = (\n+                    span_name_prefix\n+                    + url[: doc_span[0]]\n+                    + \"/_doc/:id\"\n+                    + url[doc_span[1] :]\n+                )\n+                # Put the document ID in attributes\n+                doc_id = match.group(1)\n         params = kwargs.get(\"params\", {})\n         body = kwargs.get(\"body\", None)\n \n@@ -184,6 +205,8 @@\n                     attributes[SpanAttributes.DB_STATEMENT] = str(body)\n                 if params:\n                     attributes[\"elasticsearch.params\"] = str(params)\n+                if doc_id:\n+                    attributes[\"elasticsearch.id\"] = doc_id\n                 for key, value in attributes.items():\n                     span.set_attribute(key, value)\n", "issue": "elasticsearch instrumentation creates too many span names, with unique document IDs.\n**Describe your environment**\r\nUsing `opentelemetry-instrumentation-elasticsearch` `0.24b0` with `elasticsearch` `7.14.1`\r\n\r\n**Steps to reproduce**\r\nPerform some `index()` and/or `delete()` calls with `id` parameter set\r\n\r\n**What is the expected behavior?**\r\nThe number of span names created is reasonable and finite\r\n\r\n**What is the actual behavior?**\r\nAn unbounded number of span names get created, containing the unique document names. This makes it hard to search through operations, for example in Jaeger:\r\n\r\n![screenshot](https://user-images.githubusercontent.com/426784/135645077-dde57682-5398-4e47-92d3-bf6e0eddb5ea.png)\r\n\r\nThe `datamart.test.xxxx` parts are the Elasticsearch document IDs.\r\n\r\n**Additional context**\r\nI have a patch for this incoming.\n", "before_files": [{"content": "# Copyright The OpenTelemetry Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nThis library allows tracing HTTP elasticsearch made by the\n`elasticsearch <https://elasticsearch-py.readthedocs.io/en/master/>`_ library.\n\nUsage\n-----\n\n.. code-block:: python\n\n    from opentelemetry.instrumentation.elasticsearch import ElasticsearchInstrumentor\n    import elasticsearch\n\n\n    # instrument elasticsearch\n    ElasticsearchInstrumentor().instrument()\n\n    # Using elasticsearch as normal now will automatically generate spans\n    es = elasticsearch.Elasticsearch()\n    es.index(index='my-index', doc_type='my-type', id=1, body={'my': 'data', 'timestamp': datetime.now()})\n    es.get(index='my-index', doc_type='my-type', id=1)\n\nAPI\n---\n\nElasticsearch instrumentation prefixes operation names with the string \"Elasticsearch\". This\ncan be changed to a different string by either setting the `OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX`\nenvironment variable or by passing the prefix as an argument to the instrumentor. For example,\n\n\n.. code-block:: python\n\n    ElasticsearchInstrumentor(\"my-custom-prefix\").instrument()\n\n\nThe `instrument` method accepts the following keyword args:\n\ntracer_provider (TracerProvider) - an optional tracer provider\nrequest_hook (Callable) - a function with extra user-defined logic to be performed before performing the request\n                          this function signature is:\n                          def request_hook(span: Span, method: str, url: str, kwargs)\nresponse_hook (Callable) - a function with extra user-defined logic to be performed after performing the request\n                          this function signature is:\n                          def response_hook(span: Span, response: dict)\n\nfor example:\n\n.. code: python\n\n    from opentelemetry.instrumentation.elasticsearch import ElasticsearchInstrumentor\n    import elasticsearch\n\n    def request_hook(span, method, url, kwargs):\n        if span and span.is_recording():\n            span.set_attribute(\"custom_user_attribute_from_request_hook\", \"some-value\")\n\n    def response_hook(span, response):\n        if span and span.is_recording():\n            span.set_attribute(\"custom_user_attribute_from_response_hook\", \"some-value\")\n\n    # instrument elasticsearch with request and response hooks\n    ElasticsearchInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook)\n\n    # Using elasticsearch as normal now will automatically generate spans,\n    # including user custom attributes added from the hooks\n    es = elasticsearch.Elasticsearch()\n    es.index(index='my-index', doc_type='my-type', id=1, body={'my': 'data', 'timestamp': datetime.now()})\n    es.get(index='my-index', doc_type='my-type', id=1)\n\"\"\"\n\nfrom logging import getLogger\nfrom os import environ\nfrom typing import Collection\n\nimport elasticsearch\nimport elasticsearch.exceptions\nfrom wrapt import wrap_function_wrapper as _wrap\n\nfrom opentelemetry.instrumentation.elasticsearch.package import _instruments\nfrom opentelemetry.instrumentation.elasticsearch.version import __version__\nfrom opentelemetry.instrumentation.instrumentor import BaseInstrumentor\nfrom opentelemetry.instrumentation.utils import unwrap\nfrom opentelemetry.semconv.trace import SpanAttributes\nfrom opentelemetry.trace import SpanKind, get_tracer\n\nlogger = getLogger(__name__)\n\n\n# Values to add as tags from the actual\n# payload returned by Elasticsearch, if any.\n_ATTRIBUTES_FROM_RESULT = [\n    \"found\",\n    \"timed_out\",\n    \"took\",\n]\n\n_DEFALT_OP_NAME = \"request\"\n\n\nclass ElasticsearchInstrumentor(BaseInstrumentor):\n    \"\"\"An instrumentor for elasticsearch\n    See `BaseInstrumentor`\n    \"\"\"\n\n    def __init__(self, span_name_prefix=None):\n        if not span_name_prefix:\n            span_name_prefix = environ.get(\n                \"OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX\", \"Elasticsearch\",\n            )\n        self._span_name_prefix = span_name_prefix.strip()\n        super().__init__()\n\n    def instrumentation_dependencies(self) -> Collection[str]:\n        return _instruments\n\n    def _instrument(self, **kwargs):\n        \"\"\"\n        Instruments elasticsarch module\n        \"\"\"\n        tracer_provider = kwargs.get(\"tracer_provider\")\n        tracer = get_tracer(__name__, __version__, tracer_provider)\n        request_hook = kwargs.get(\"request_hook\")\n        response_hook = kwargs.get(\"response_hook\")\n        _wrap(\n            elasticsearch,\n            \"Transport.perform_request\",\n            _wrap_perform_request(\n                tracer, self._span_name_prefix, request_hook, response_hook\n            ),\n        )\n\n    def _uninstrument(self, **kwargs):\n        unwrap(elasticsearch.Transport, \"perform_request\")\n\n\ndef _wrap_perform_request(\n    tracer, span_name_prefix, request_hook=None, response_hook=None\n):\n    # pylint: disable=R0912\n    def wrapper(wrapped, _, args, kwargs):\n        method = url = None\n        try:\n            method, url, *_ = args\n        except IndexError:\n            logger.warning(\n                \"expected perform_request to receive two positional arguments. \"\n                \"Got %d\",\n                len(args),\n            )\n\n        op_name = span_name_prefix + (url or method or _DEFALT_OP_NAME)\n        params = kwargs.get(\"params\", {})\n        body = kwargs.get(\"body\", None)\n\n        with tracer.start_as_current_span(\n            op_name, kind=SpanKind.CLIENT,\n        ) as span:\n\n            if callable(request_hook):\n                request_hook(span, method, url, kwargs)\n\n            if span.is_recording():\n                attributes = {\n                    SpanAttributes.DB_SYSTEM: \"elasticsearch\",\n                }\n                if url:\n                    attributes[\"elasticsearch.url\"] = url\n                if method:\n                    attributes[\"elasticsearch.method\"] = method\n                if body:\n                    attributes[SpanAttributes.DB_STATEMENT] = str(body)\n                if params:\n                    attributes[\"elasticsearch.params\"] = str(params)\n                for key, value in attributes.items():\n                    span.set_attribute(key, value)\n\n            rv = wrapped(*args, **kwargs)\n            if isinstance(rv, dict) and span.is_recording():\n                for member in _ATTRIBUTES_FROM_RESULT:\n                    if member in rv:\n                        span.set_attribute(\n                            f\"elasticsearch.{member}\", str(rv[member]),\n                        )\n\n            if callable(response_hook):\n                response_hook(span, rv)\n            return rv\n\n    return wrapper\n", "path": "instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py"}], "after_files": [{"content": "# Copyright The OpenTelemetry Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nThis library allows tracing HTTP elasticsearch made by the\n`elasticsearch <https://elasticsearch-py.readthedocs.io/en/master/>`_ library.\n\nUsage\n-----\n\n.. code-block:: python\n\n    from opentelemetry.instrumentation.elasticsearch import ElasticsearchInstrumentor\n    import elasticsearch\n\n\n    # instrument elasticsearch\n    ElasticsearchInstrumentor().instrument()\n\n    # Using elasticsearch as normal now will automatically generate spans\n    es = elasticsearch.Elasticsearch()\n    es.index(index='my-index', doc_type='my-type', id=1, body={'my': 'data', 'timestamp': datetime.now()})\n    es.get(index='my-index', doc_type='my-type', id=1)\n\nAPI\n---\n\nElasticsearch instrumentation prefixes operation names with the string \"Elasticsearch\". This\ncan be changed to a different string by either setting the `OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX`\nenvironment variable or by passing the prefix as an argument to the instrumentor. For example,\n\n\n.. code-block:: python\n\n    ElasticsearchInstrumentor(\"my-custom-prefix\").instrument()\n\n\nThe `instrument` method accepts the following keyword args:\n\ntracer_provider (TracerProvider) - an optional tracer provider\nrequest_hook (Callable) - a function with extra user-defined logic to be performed before performing the request\n                          this function signature is:\n                          def request_hook(span: Span, method: str, url: str, kwargs)\nresponse_hook (Callable) - a function with extra user-defined logic to be performed after performing the request\n                          this function signature is:\n                          def response_hook(span: Span, response: dict)\n\nfor example:\n\n.. code: python\n\n    from opentelemetry.instrumentation.elasticsearch import ElasticsearchInstrumentor\n    import elasticsearch\n\n    def request_hook(span, method, url, kwargs):\n        if span and span.is_recording():\n            span.set_attribute(\"custom_user_attribute_from_request_hook\", \"some-value\")\n\n    def response_hook(span, response):\n        if span and span.is_recording():\n            span.set_attribute(\"custom_user_attribute_from_response_hook\", \"some-value\")\n\n    # instrument elasticsearch with request and response hooks\n    ElasticsearchInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook)\n\n    # Using elasticsearch as normal now will automatically generate spans,\n    # including user custom attributes added from the hooks\n    es = elasticsearch.Elasticsearch()\n    es.index(index='my-index', doc_type='my-type', id=1, body={'my': 'data', 'timestamp': datetime.now()})\n    es.get(index='my-index', doc_type='my-type', id=1)\n\"\"\"\n\nimport re\nfrom logging import getLogger\nfrom os import environ\nfrom typing import Collection\n\nimport elasticsearch\nimport elasticsearch.exceptions\nfrom wrapt import wrap_function_wrapper as _wrap\n\nfrom opentelemetry.instrumentation.elasticsearch.package import _instruments\nfrom opentelemetry.instrumentation.elasticsearch.version import __version__\nfrom opentelemetry.instrumentation.instrumentor import BaseInstrumentor\nfrom opentelemetry.instrumentation.utils import unwrap\nfrom opentelemetry.semconv.trace import SpanAttributes\nfrom opentelemetry.trace import SpanKind, get_tracer\n\nlogger = getLogger(__name__)\n\n\n# Values to add as tags from the actual\n# payload returned by Elasticsearch, if any.\n_ATTRIBUTES_FROM_RESULT = [\n    \"found\",\n    \"timed_out\",\n    \"took\",\n]\n\n_DEFAULT_OP_NAME = \"request\"\n\n\nclass ElasticsearchInstrumentor(BaseInstrumentor):\n    \"\"\"An instrumentor for elasticsearch\n    See `BaseInstrumentor`\n    \"\"\"\n\n    def __init__(self, span_name_prefix=None):\n        if not span_name_prefix:\n            span_name_prefix = environ.get(\n                \"OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX\", \"Elasticsearch\",\n            )\n        self._span_name_prefix = span_name_prefix.strip()\n        super().__init__()\n\n    def instrumentation_dependencies(self) -> Collection[str]:\n        return _instruments\n\n    def _instrument(self, **kwargs):\n        \"\"\"\n        Instruments elasticsarch module\n        \"\"\"\n        tracer_provider = kwargs.get(\"tracer_provider\")\n        tracer = get_tracer(__name__, __version__, tracer_provider)\n        request_hook = kwargs.get(\"request_hook\")\n        response_hook = kwargs.get(\"response_hook\")\n        _wrap(\n            elasticsearch,\n            \"Transport.perform_request\",\n            _wrap_perform_request(\n                tracer, self._span_name_prefix, request_hook, response_hook\n            ),\n        )\n\n    def _uninstrument(self, **kwargs):\n        unwrap(elasticsearch.Transport, \"perform_request\")\n\n\n_regex_doc_url = re.compile(r\"/_doc/([^/]+)\")\n\n\ndef _wrap_perform_request(\n    tracer, span_name_prefix, request_hook=None, response_hook=None\n):\n    # pylint: disable=R0912\n    def wrapper(wrapped, _, args, kwargs):\n        method = url = None\n        try:\n            method, url, *_ = args\n        except IndexError:\n            logger.warning(\n                \"expected perform_request to receive two positional arguments. \"\n                \"Got %d\",\n                len(args),\n            )\n\n        op_name = span_name_prefix + (url or method or _DEFAULT_OP_NAME)\n        doc_id = None\n        if url:\n            # TODO: This regex-based solution avoids creating an unbounded number of span names, but should be replaced by instrumenting individual Elasticsearch methods instead of Transport.perform_request()\n            # A limitation of the regex is that only the '_doc' mapping type is supported. Mapping types are deprecated since Elasticsearch 7\n            # https://github.com/open-telemetry/opentelemetry-python-contrib/issues/708\n            match = _regex_doc_url.search(url)\n            if match is not None:\n                # Remove the full document ID from the URL\n                doc_span = match.span()\n                op_name = (\n                    span_name_prefix\n                    + url[: doc_span[0]]\n                    + \"/_doc/:id\"\n                    + url[doc_span[1] :]\n                )\n                # Put the document ID in attributes\n                doc_id = match.group(1)\n        params = kwargs.get(\"params\", {})\n        body = kwargs.get(\"body\", None)\n\n        with tracer.start_as_current_span(\n            op_name, kind=SpanKind.CLIENT,\n        ) as span:\n\n            if callable(request_hook):\n                request_hook(span, method, url, kwargs)\n\n            if span.is_recording():\n                attributes = {\n                    SpanAttributes.DB_SYSTEM: \"elasticsearch\",\n                }\n                if url:\n                    attributes[\"elasticsearch.url\"] = url\n                if method:\n                    attributes[\"elasticsearch.method\"] = method\n                if body:\n                    attributes[SpanAttributes.DB_STATEMENT] = str(body)\n                if params:\n                    attributes[\"elasticsearch.params\"] = str(params)\n                if doc_id:\n                    attributes[\"elasticsearch.id\"] = doc_id\n                for key, value in attributes.items():\n                    span.set_attribute(key, value)\n\n            rv = wrapped(*args, **kwargs)\n            if isinstance(rv, dict) and span.is_recording():\n                for member in _ATTRIBUTES_FROM_RESULT:\n                    if member in rv:\n                        span.set_attribute(\n                            f\"elasticsearch.{member}\", str(rv[member]),\n                        )\n\n            if callable(response_hook):\n                response_hook(span, rv)\n            return rv\n\n    return wrapper\n", "path": "instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py"}]}