Skip to content

Commit e56584b

Browse files
authored
Merge branch 'main' into issue_2478
2 parents 7c5ad67 + 66a107f commit e56584b

File tree

33 files changed

+628
-137
lines changed

33 files changed

+628
-137
lines changed

CHANGELOG.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3939

4040
### Fixed
4141

42+
- `opentelemetry-instrumentation-dbapi` Fix compatibility with Psycopg3 to extract libpq build version (#2500)[https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2500]
4243
- `opentelemetry-instrumentation-grpc` AioClientInterceptor should propagate with a Metadata object
4344
([#2363](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2363))
4445
- `opentelemetry-instrumentation-boto3sqs` Instrument Session and resource
@@ -51,6 +52,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5152
([#2461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2461))
5253
- Remove SDK dependency from opentelemetry-instrumentation-grpc
5354
([#2474](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2474))
55+
- `opentelemetry-instrumentation-elasticsearch` Improved support for version 8
56+
([#2420](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2420))
57+
- `opentelemetry-instrumentation-asyncio` Check for __name__ attribute in the coroutine
58+
([#2521](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2521))
5459

5560
## Version 1.24.0/0.45b0 (2024-03-28)
5661

@@ -122,7 +127,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
122127
([#1959](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1959))
123128
- `opentelemetry-resource-detector-azure` Added dependency for Cloud Resource ID attribute
124129
([#2072](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2072))
125-
130+
126131
## Version 1.21.0/0.42b0 (2023-11-01)
127132

128133
### Added
@@ -1536,4 +1541,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15361541
- `opentelemetry-resource-detector-azure` Suppress instrumentation for `urllib` call
15371542
([#2178](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2178))
15381543
- AwsLambdaInstrumentor handles and re-raises function exception ([#2245](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2245))
1539-

CONTRIBUTING.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ You can run `tox` with the following arguments:
6767
`black` and `isort` are executed when `tox -e lint` is run. The reported errors can be tedious to fix manually.
6868
An easier way to do so is:
6969

70-
1. Run `.tox/lint-some-package/bin/black .`
71-
2. Run `.tox/lint-some-package/bin/isort .`
70+
1. Run `.tox/lint/bin/black .`
71+
2. Run `.tox/lint/bin/isort .`
7272

7373
Or you can call formatting and linting in one command by [pre-commit](https://pre-commit.com/):
7474

gen-requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
-c dev-requirements.txt
22
astor==0.8.1
3-
jinja2==3.1.3
3+
jinja2==3.1.4
44
markupsafe==2.0.1
55
isort
66
black

instrumentation/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
| [opentelemetry-instrumentation-confluent-kafka](./opentelemetry-instrumentation-confluent-kafka) | confluent-kafka >= 1.8.2, <= 2.3.0 | No | experimental
1818
| [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi | No | experimental
1919
| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes | experimental
20-
| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 2.0 | No | experimental
20+
| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 6.0 | No | experimental
2121
| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 4.0.0 | Yes | experimental
2222
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | experimental
2323
| [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration

instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ def trace_item(self, coro_or_future):
261261
return coro_or_future
262262

263263
async def trace_coroutine(self, coro):
264+
if not hasattr(coro, "__name__"):
265+
return coro
264266
start = default_timer()
265267
attr = {
266268
"type": "coroutine",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
import asyncio
15+
from unittest.mock import patch
16+
17+
# pylint: disable=no-name-in-module
18+
from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor
19+
from opentelemetry.instrumentation.asyncio.environment_variables import (
20+
OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE,
21+
)
22+
from opentelemetry.test.test_base import TestBase
23+
from opentelemetry.trace import get_tracer
24+
25+
26+
class TestAsyncioAnext(TestBase):
27+
@patch.dict(
28+
"os.environ",
29+
{OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "async_func"},
30+
)
31+
def setUp(self):
32+
super().setUp()
33+
AsyncioInstrumentor().instrument()
34+
self._tracer = get_tracer(
35+
__name__,
36+
)
37+
38+
def tearDown(self):
39+
super().tearDown()
40+
AsyncioInstrumentor().uninstrument()
41+
42+
# Asyncio anext() does not have __name__ attribute, which is used to determine if the coroutine should be traced.
43+
# This test is to ensure that the instrumentation does not break when the coroutine does not have __name__ attribute.
44+
def test_asyncio_anext(self):
45+
async def main():
46+
async def async_gen():
47+
for it in range(2):
48+
yield it
49+
50+
async_gen_instance = async_gen()
51+
agen = anext(async_gen_instance)
52+
await asyncio.create_task(agen)
53+
54+
asyncio.run(main())
55+
spans = self.memory_exporter.get_finished_spans()
56+
self.assertEqual(len(spans), 0)

instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
request_hook (Callable) - a function with extra user-defined logic to be performed before performing the request
5252
this function signature is: def request_hook(span: Span, service_name: str, operation_name: str, api_params: dict) -> None
5353
response_hook (Callable) - a function with extra user-defined logic to be performed after performing the request
54-
this function signature is: def request_hook(span: Span, service_name: str, operation_name: str, result: dict) -> None
54+
this function signature is: def response_hook(span: Span, service_name: str, operation_name: str, result: dict) -> None
5555
5656
for example:
5757

instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -427,12 +427,19 @@ def traced_execution(
427427
if args and self._commenter_enabled:
428428
try:
429429
args_list = list(args)
430+
if hasattr(self._connect_module, "__libpq_version__"):
431+
libpq_version = self._connect_module.__libpq_version__
432+
else:
433+
libpq_version = (
434+
self._connect_module.pq.__build_version__
435+
)
436+
430437
commenter_data = {
431438
# Psycopg2/framework information
432439
"db_driver": f"psycopg2:{self._connect_module.__version__.split(' ')[0]}",
433440
"dbapi_threadsafety": self._connect_module.threadsafety,
434441
"dbapi_level": self._connect_module.apilevel,
435-
"libpq_version": self._connect_module.__libpq_version__,
442+
"libpq_version": libpq_version,
436443
"driver_paramstyle": self._connect_module.paramstyle,
437444
}
438445
if self._commenter_options.get(

instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py

+26
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,32 @@ def test_executemany_comment(self):
275275
r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
276276
)
277277

278+
def test_compatible_build_version_psycopg_psycopg2_libpq(self):
279+
connect_module = mock.MagicMock()
280+
connect_module.__version__ = mock.MagicMock()
281+
connect_module.pq = mock.MagicMock()
282+
connect_module.pq.__build_version__ = 123
283+
connect_module.apilevel = 123
284+
connect_module.threadsafety = 123
285+
connect_module.paramstyle = "test"
286+
287+
db_integration = dbapi.DatabaseApiIntegration(
288+
"testname",
289+
"testcomponent",
290+
enable_commenter=True,
291+
commenter_options={"db_driver": False, "dbapi_level": False},
292+
connect_module=connect_module,
293+
)
294+
mock_connection = db_integration.wrapped_connection(
295+
mock_connect, {}, {}
296+
)
297+
cursor = mock_connection.cursor()
298+
cursor.executemany("Select 1;")
299+
self.assertRegex(
300+
cursor.query,
301+
r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
302+
)
303+
278304
def test_executemany_flask_integration_comment(self):
279305
connect_module = mock.MagicMock()
280306
connect_module.__version__ = mock.MagicMock()

instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py

+54-7
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def response_hook(span, response):
9494
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
9595
from opentelemetry.instrumentation.utils import unwrap
9696
from opentelemetry.semconv.trace import SpanAttributes
97-
from opentelemetry.trace import SpanKind, get_tracer
97+
from opentelemetry.trace import SpanKind, Status, StatusCode, get_tracer
9898

9999
from .utils import sanitize_body
100100

@@ -103,6 +103,7 @@ def response_hook(span, response):
103103
es_transport_split = elasticsearch.VERSION[0] > 7
104104
if es_transport_split:
105105
import elastic_transport
106+
from elastic_transport._models import DefaultType
106107

107108
logger = getLogger(__name__)
108109

@@ -173,7 +174,12 @@ def _instrument(self, **kwargs):
173174

174175
def _uninstrument(self, **kwargs):
175176
# pylint: disable=no-member
176-
unwrap(elasticsearch.Transport, "perform_request")
177+
transport_class = (
178+
elastic_transport.Transport
179+
if es_transport_split
180+
else elasticsearch.Transport
181+
)
182+
unwrap(transport_class, "perform_request")
177183

178184

179185
_regex_doc_url = re.compile(r"/_doc/([^/]+)")
@@ -182,6 +188,7 @@ def _uninstrument(self, **kwargs):
182188
_regex_search_url = re.compile(r"/([^/]+)/_search[/]?")
183189

184190

191+
# pylint: disable=too-many-statements
185192
def _wrap_perform_request(
186193
tracer,
187194
span_name_prefix,
@@ -234,7 +241,22 @@ def wrapper(wrapped, _, args, kwargs):
234241
kind=SpanKind.CLIENT,
235242
) as span:
236243
if callable(request_hook):
237-
request_hook(span, method, url, kwargs)
244+
# elasticsearch 8 changed the parameters quite a bit
245+
if es_transport_split:
246+
247+
def normalize_kwargs(k, v):
248+
if isinstance(v, DefaultType):
249+
v = str(v)
250+
elif isinstance(v, elastic_transport.HttpHeaders):
251+
v = dict(v)
252+
return (k, v)
253+
254+
hook_kwargs = dict(
255+
normalize_kwargs(k, v) for k, v in kwargs.items()
256+
)
257+
else:
258+
hook_kwargs = kwargs
259+
request_hook(span, method, url, hook_kwargs)
238260

239261
if span.is_recording():
240262
attributes = {
@@ -260,16 +282,41 @@ def wrapper(wrapped, _, args, kwargs):
260282
span.set_attribute(key, value)
261283

262284
rv = wrapped(*args, **kwargs)
263-
if isinstance(rv, dict) and span.is_recording():
285+
286+
body = rv.body if es_transport_split else rv
287+
if isinstance(body, dict) and span.is_recording():
264288
for member in _ATTRIBUTES_FROM_RESULT:
265-
if member in rv:
289+
if member in body:
266290
span.set_attribute(
267291
f"elasticsearch.{member}",
268-
str(rv[member]),
292+
str(body[member]),
293+
)
294+
295+
# since the transport split the raising of exceptions that set the error status
296+
# are called after this code so need to set error status manually
297+
if es_transport_split and span.is_recording():
298+
if not (method == "HEAD" and rv.meta.status == 404) and (
299+
not 200 <= rv.meta.status < 299
300+
):
301+
exception = elasticsearch.exceptions.HTTP_EXCEPTIONS.get(
302+
rv.meta.status, elasticsearch.exceptions.ApiError
303+
)
304+
message = str(body)
305+
if isinstance(body, dict):
306+
error = body.get("error", message)
307+
if isinstance(error, dict) and "type" in error:
308+
error = error["type"]
309+
message = error
310+
311+
span.set_status(
312+
Status(
313+
status_code=StatusCode.ERROR,
314+
description=f"{exception.__name__}: {message}",
269315
)
316+
)
270317

271318
if callable(response_hook):
272-
response_hook(span, rv)
319+
response_hook(span, body)
273320
return rv
274321

275322
return wrapper

instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/package.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313
# limitations under the License.
1414

1515

16-
_instruments = ("elasticsearch >= 2.0",)
16+
_instruments = ("elasticsearch >= 6.0",)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
asgiref==3.7.2
2+
attrs==23.2.0
3+
Deprecated==1.2.14
4+
elasticsearch==8.12.1
5+
elasticsearch-dsl==8.12.0
6+
elastic-transport==8.12.0
7+
importlib-metadata==6.11.0
8+
iniconfig==2.0.0
9+
packaging==23.2
10+
pluggy==1.4.0
11+
py==1.11.0
12+
py-cpuinfo==9.0.0
13+
pytest==7.1.3
14+
pytest-benchmark==4.0.0
15+
python-dateutil==2.8.2
16+
six==1.16.0
17+
tomli==2.0.1
18+
typing_extensions==4.10.0
19+
urllib3==2.2.1
20+
wrapt==1.16.0
21+
zipp==3.17.0
22+
-e opentelemetry-instrumentation
23+
-e instrumentation/opentelemetry-instrumentation-elasticsearch

instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py

+6
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,9 @@ class Index:
3131
dsl_index_span_name = "Elasticsearch/test-index/doc/2"
3232
dsl_index_url = "/test-index/doc/2"
3333
dsl_search_method = "GET"
34+
35+
perform_request_mock_path = "elasticsearch.connection.http_urllib3.Urllib3HttpConnection.perform_request"
36+
37+
38+
def mock_response(body: str, status_code: int = 200):
39+
return (status_code, {}, body)

instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py

+6
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,9 @@ class Index:
2929
dsl_index_span_name = "Elasticsearch/test-index/_doc/:id"
3030
dsl_index_url = "/test-index/_doc/2"
3131
dsl_search_method = "POST"
32+
33+
perform_request_mock_path = "elasticsearch.connection.http_urllib3.Urllib3HttpConnection.perform_request"
34+
35+
36+
def mock_response(body: str, status_code: int = 200):
37+
return (status_code, {}, body)

instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es8.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from elastic_transport import ApiResponseMeta, HttpHeaders
16+
from elastic_transport._node import NodeApiResponse
1517
from elasticsearch_dsl import Document, Keyword, Text
1618

1719

@@ -36,6 +38,23 @@ class Index:
3638
}
3739
}
3840
dsl_index_result = (1, {}, '{"result": "created"}')
39-
dsl_index_span_name = "Elasticsearch/test-index/_doc/2"
41+
dsl_index_span_name = "Elasticsearch/test-index/_doc/:id"
4042
dsl_index_url = "/test-index/_doc/2"
4143
dsl_search_method = "POST"
44+
45+
perform_request_mock_path = (
46+
"elastic_transport._node._http_urllib3.Urllib3HttpNode.perform_request"
47+
)
48+
49+
50+
def mock_response(body: str, status_code: int = 200):
51+
return NodeApiResponse(
52+
ApiResponseMeta(
53+
status=status_code,
54+
headers=HttpHeaders({}),
55+
duration=100,
56+
http_version="1.1",
57+
node="node",
58+
),
59+
body.encode(),
60+
)

0 commit comments

Comments
 (0)