From 1d3dea0475af94399f6b251c67712272831d40ae Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Sat, 4 May 2024 16:47:19 -0400 Subject: [PATCH 01/20] Remove SDK dependency from opentelemetry-instrumentation-grpc (#2474) Co-authored-by: Diego Hurtado Co-authored-by: Riccardo Magliocchetti --- CHANGELOG.md | 2 ++ .../opentelemetry-instrumentation-grpc/pyproject.toml | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb97433f1..dff32bafca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2418](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2418)) - Use sqlalchemy version in sqlalchemy commenter instead of opentelemetry library version ([#2404](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2404)) +- Remove SDK dependency from opentelemetry-instrumentation-grpc + ([#2474](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2474)) ## Version 1.24.0/0.45b0 (2024-03-28) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/pyproject.toml b/instrumentation/opentelemetry-instrumentation-grpc/pyproject.toml index 750f76270a..7deffb71e7 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-grpc/pyproject.toml @@ -26,7 +26,6 @@ classifiers = [ dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.46b0.dev", - "opentelemetry-sdk ~= 1.12", "opentelemetry-semantic-conventions == 0.46b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] From 0a231e57f9722e6101194c6b38695addf23ab950 Mon Sep 17 00:00:00 2001 From: Akshay Awate Date: Mon, 6 May 2024 22:21:39 +0530 Subject: [PATCH 02/20] Update README.rst (#2499) * Update README.rst Fix README hyperlink syntax. * updated README --- opentelemetry-contrib-instrumentations/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-contrib-instrumentations/README.rst b/opentelemetry-contrib-instrumentations/README.rst index 4e581c0a63..e0a76806e1 100644 --- a/opentelemetry-contrib-instrumentations/README.rst +++ b/opentelemetry-contrib-instrumentations/README.rst @@ -15,7 +15,7 @@ Installation This package installs all instrumentation packages hosted by the OpenTelemetry Python Contrib repository. -The list of packages can be found (here)[https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation] +The list of packages can be found `here `_. References From bc804a3b07b7af45bbb352d553591d3a72845376 Mon Sep 17 00:00:00 2001 From: Allen Kim Date: Wed, 8 May 2024 08:40:21 +0900 Subject: [PATCH 03/20] Bugfix/check future cancelled (#2461) * Calling the exception() method when future is in the cancelled state is causing a CancelledError Calling the exception() method when future is in the cancelled state is causing a CancelledError. we should check the cancelled state first and call f.exception() only if it's not cancelled. * modify lint * modify lint * Update CHANGELOG.md * remove init() * add future cancelled test code * add future cancelled test code * add future cancelled test code * add future cancelled test code * add future cancelled test code * add future cancelled test code * lint * lint * remove if condition * modify test code * lint * lint * remove pytest --------- Co-authored-by: Diego Hurtado --- CHANGELOG.md | 2 + .../instrumentation/asyncio/__init__.py | 24 +++----- .../tests/test_asyncio_future_cancellation.py | 60 +++++++++++++++++++ 3 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_future_cancellation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index dff32bafca..28e8a85c26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2418](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2418)) - Use sqlalchemy version in sqlalchemy commenter instead of opentelemetry library version ([#2404](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2404)) +- `opentelemetry-instrumentation-asyncio` Check for cancelledException in the future + ([#2461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2461)) - Remove SDK dependency from opentelemetry-instrumentation-grpc ([#2474](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2474)) diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py index 68e3d0839f..72aa5fd2aa 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py @@ -116,21 +116,11 @@ class AsyncioInstrumentor(BaseInstrumentor): "run_coroutine_threadsafe", ] - def __init__(self): - super().__init__() - self.process_duration_histogram = None - self.process_created_counter = None - - self._tracer = None - self._meter = None - self._coros_name_to_trace: set = set() - self._to_thread_name_to_trace: set = set() - self._future_active_enabled: bool = False - def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): + # pylint: disable=attribute-defined-outside-init self._tracer = get_tracer( __name__, __version__, kwargs.get("tracer_provider") ) @@ -307,13 +297,17 @@ def trace_future(self, future): ) def callback(f): - exception = f.exception() attr = { "type": "future", + "state": ( + "cancelled" + if f.cancelled() + else determine_state(f.exception()) + ), } - state = determine_state(exception) - attr["state"] = state - self.record_process(start, attr, span, exception) + self.record_process( + start, attr, span, None if f.cancelled() else f.exception() + ) future.add_done_callback(callback) return future diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_future_cancellation.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_future_cancellation.py new file mode 100644 index 0000000000..f8f4e5f230 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_future_cancellation.py @@ -0,0 +1,60 @@ +import asyncio +from unittest.mock import patch + +from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor +from opentelemetry.instrumentation.asyncio.environment_variables import ( + OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED, +) +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import get_tracer + + +class TestTraceFuture(TestBase): + @patch.dict( + "os.environ", {OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED: "true"} + ) + def setUp(self): + super().setUp() + self._tracer = get_tracer( + __name__, + ) + self.instrumentor = AsyncioInstrumentor() + self.instrumentor.instrument() + + def tearDown(self): + super().tearDown() + self.instrumentor.uninstrument() + + def test_trace_future_cancelled(self): + async def future_cancelled(): + with self._tracer.start_as_current_span("root"): + future = asyncio.Future() + future = self.instrumentor.trace_future(future) + future.cancel() + + try: + asyncio.run(future_cancelled()) + except asyncio.CancelledError as exc: + self.assertEqual(isinstance(exc, asyncio.CancelledError), True) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + self.assertEqual(spans[0].name, "root") + self.assertEqual(spans[1].name, "asyncio future") + + metrics = ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics + ) + self.assertEqual(len(metrics), 2) + + self.assertEqual(metrics[0].name, "asyncio.process.duration") + self.assertEqual( + metrics[0].data.data_points[0].attributes["state"], "cancelled" + ) + + self.assertEqual(metrics[1].name, "asyncio.process.created") + self.assertEqual( + metrics[1].data.data_points[0].attributes["state"], "cancelled" + ) From 935f51eb8eaa54fc7d2c7dd6718906fe782cf223 Mon Sep 17 00:00:00 2001 From: hyfj44255 Date: Thu, 9 May 2024 23:14:08 +0800 Subject: [PATCH 04/20] upgrade pymongo to avoid CWE-125 vulnerability issue (#2497) Signed-off-by: Yang, Robin --- .../opentelemetry-instrumentation-pymongo/test-requirements.txt | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt index 01d48e8dc4..0ad6375a14 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt @@ -8,7 +8,7 @@ packaging==23.2 pluggy==1.4.0 py==1.11.0 py-cpuinfo==9.0.0 -pymongo==4.6.2 +pymongo==4.6.3 pytest==7.1.3 pytest-benchmark==4.0.0 tomli==2.0.1 diff --git a/tox.ini b/tox.ini index ed74e485cd..37a1727935 100644 --- a/tox.ini +++ b/tox.ini @@ -1250,7 +1250,7 @@ deps = psycopg2==2.9.9 psycopg2-binary==2.9.9 pycparser==2.21 - pymongo==4.6.2 + pymongo==4.6.3 PyMySQL==0.10.1 PyNaCl==1.5.0 # prerequisite: install unixodbc From eabceff062372d3dd17d2dc790d34919f0c94b5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 May 2024 13:53:12 -0500 Subject: [PATCH 05/20] Bump jinja2 from 3.1.3 to 3.1.4 (#2503) Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4) --- updated-dependencies: - dependency-name: jinja2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Diego Hurtado --- gen-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gen-requirements.txt b/gen-requirements.txt index de84b72c1e..b2d5c4f695 100644 --- a/gen-requirements.txt +++ b/gen-requirements.txt @@ -1,6 +1,6 @@ -c dev-requirements.txt astor==0.8.1 -jinja2==3.1.3 +jinja2==3.1.4 markupsafe==2.0.1 isort black From 46d2ce6acea9a1a6cb1a4d4c863077002f5f7d21 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 10 May 2024 18:51:32 +0200 Subject: [PATCH 06/20] Reinstate tox -e lint (#2482) --- CONTRIBUTING.md | 4 ++-- tox.ini | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3de25a4e67..743449c2a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,8 +67,8 @@ You can run `tox` with the following arguments: `black` and `isort` are executed when `tox -e lint` is run. The reported errors can be tedious to fix manually. An easier way to do so is: -1. Run `.tox/lint-some-package/bin/black .` -2. Run `.tox/lint-some-package/bin/isort .` +1. Run `.tox/lint/bin/black .` +2. Run `.tox/lint/bin/isort .` Or you can call formatting and linting in one command by [pre-commit](https://pre-commit.com/): diff --git a/tox.ini b/tox.ini index 37a1727935..ae11e0f24f 100644 --- a/tox.ini +++ b/tox.ini @@ -1189,6 +1189,17 @@ changedir = docs commands = sphinx-build -E -a -W -b html -T . _build/html +[testenv:lint] +basepython: python3 +recreate = True +deps = + -r dev-requirements.txt + +commands = + black --config {toxinidir}/pyproject.toml {{toxinidir}} --diff --check + isort --settings-path {toxinidir}/.isort.cfg {{toxinidir}} --diff --check-only + flake8 --config {toxinidir}/.flake8 {toxinidir} + [testenv:spellcheck] basepython: python3 recreate = True From 9b7197d3b989dac0a7965b6c55b2ee902b3f5b45 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Tue, 14 May 2024 03:24:24 +1000 Subject: [PATCH 07/20] docs: fix name of response hook signature in botocore instrumentation (#2512) --- .../src/opentelemetry/instrumentation/botocore/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py index 36b973e318..0481b248aa 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py @@ -51,7 +51,7 @@ request_hook (Callable) - a function with extra user-defined logic to be performed before performing the request this function signature is: def request_hook(span: Span, service_name: str, operation_name: str, api_params: dict) -> None response_hook (Callable) - a function with extra user-defined logic to be performed after performing the request -this function signature is: def request_hook(span: Span, service_name: str, operation_name: str, result: dict) -> None +this function signature is: def response_hook(span: Span, service_name: str, operation_name: str, result: dict) -> None for example: From 6a40ffd90512e3e4636bddb20728f8f680b69f8a Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Tue, 14 May 2024 21:59:41 +0200 Subject: [PATCH 08/20] elasticsearch: tests against elasticsearch 8 (#2420) * elasticsearch: bump handled version to 6.0 After 4de0e5659d451baee65af412242b95f174444d87 * elasticsearch: tests against elasticsearch 8 --- CHANGELOG.md | 2 + instrumentation/README.md | 2 +- .../instrumentation/elasticsearch/__init__.py | 61 ++++++++-- .../instrumentation/elasticsearch/package.py | 2 +- .../test-requirements-2.txt | 23 ++++ .../tests/helpers_es6.py | 6 + .../tests/helpers_es7.py | 6 + .../tests/helpers_es8.py | 21 +++- .../tests/test_elasticsearch.py | 112 ++++++++++++------ tox.ini | 8 +- 10 files changed, 191 insertions(+), 52 deletions(-) create mode 100644 instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e8a85c26..d10983c10b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2461)) - Remove SDK dependency from opentelemetry-instrumentation-grpc ([#2474](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2474)) +- `opentelemetry-instrumentation-elasticsearch` Improved support for version 8 + ([#2420](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2420)) ## Version 1.24.0/0.45b0 (2024-03-28) diff --git a/instrumentation/README.md b/instrumentation/README.md index c73d0f7c0a..5dfed03e9a 100644 --- a/instrumentation/README.md +++ b/instrumentation/README.md @@ -17,7 +17,7 @@ | [opentelemetry-instrumentation-confluent-kafka](./opentelemetry-instrumentation-confluent-kafka) | confluent-kafka >= 1.8.2, <= 2.3.0 | No | experimental | [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi | No | experimental | [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes | experimental -| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 2.0 | No | experimental +| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 6.0 | No | experimental | [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 4.0.0 | Yes | experimental | [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | experimental | [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py index ceb50cac56..acf4596fb0 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py @@ -94,7 +94,7 @@ def response_hook(span, response): from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace import SpanKind, get_tracer +from opentelemetry.trace import SpanKind, Status, StatusCode, get_tracer from .utils import sanitize_body @@ -103,6 +103,7 @@ def response_hook(span, response): es_transport_split = elasticsearch.VERSION[0] > 7 if es_transport_split: import elastic_transport + from elastic_transport._models import DefaultType logger = getLogger(__name__) @@ -173,7 +174,12 @@ def _instrument(self, **kwargs): def _uninstrument(self, **kwargs): # pylint: disable=no-member - unwrap(elasticsearch.Transport, "perform_request") + transport_class = ( + elastic_transport.Transport + if es_transport_split + else elasticsearch.Transport + ) + unwrap(transport_class, "perform_request") _regex_doc_url = re.compile(r"/_doc/([^/]+)") @@ -182,6 +188,7 @@ def _uninstrument(self, **kwargs): _regex_search_url = re.compile(r"/([^/]+)/_search[/]?") +# pylint: disable=too-many-statements def _wrap_perform_request( tracer, span_name_prefix, @@ -234,7 +241,22 @@ def wrapper(wrapped, _, args, kwargs): kind=SpanKind.CLIENT, ) as span: if callable(request_hook): - request_hook(span, method, url, kwargs) + # elasticsearch 8 changed the parameters quite a bit + if es_transport_split: + + def normalize_kwargs(k, v): + if isinstance(v, DefaultType): + v = str(v) + elif isinstance(v, elastic_transport.HttpHeaders): + v = dict(v) + return (k, v) + + hook_kwargs = dict( + normalize_kwargs(k, v) for k, v in kwargs.items() + ) + else: + hook_kwargs = kwargs + request_hook(span, method, url, hook_kwargs) if span.is_recording(): attributes = { @@ -260,16 +282,41 @@ def wrapper(wrapped, _, args, kwargs): span.set_attribute(key, value) rv = wrapped(*args, **kwargs) - if isinstance(rv, dict) and span.is_recording(): + + body = rv.body if es_transport_split else rv + if isinstance(body, dict) and span.is_recording(): for member in _ATTRIBUTES_FROM_RESULT: - if member in rv: + if member in body: span.set_attribute( f"elasticsearch.{member}", - str(rv[member]), + str(body[member]), + ) + + # since the transport split the raising of exceptions that set the error status + # are called after this code so need to set error status manually + if es_transport_split and span.is_recording(): + if not (method == "HEAD" and rv.meta.status == 404) and ( + not 200 <= rv.meta.status < 299 + ): + exception = elasticsearch.exceptions.HTTP_EXCEPTIONS.get( + rv.meta.status, elasticsearch.exceptions.ApiError + ) + message = str(body) + if isinstance(body, dict): + error = body.get("error", message) + if isinstance(error, dict) and "type" in error: + error = error["type"] + message = error + + span.set_status( + Status( + status_code=StatusCode.ERROR, + description=f"{exception.__name__}: {message}", ) + ) if callable(response_hook): - response_hook(span, rv) + response_hook(span, body) return rv return wrapper diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/package.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/package.py index 5b0fb7e6ea..bae644a70b 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/package.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/package.py @@ -13,4 +13,4 @@ # limitations under the License. -_instruments = ("elasticsearch >= 2.0",) +_instruments = ("elasticsearch >= 6.0",) diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt new file mode 100644 index 0000000000..23d87f93dd --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt @@ -0,0 +1,23 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +elasticsearch==8.12.1 +elasticsearch-dsl==8.12.0 +elastic-transport==8.12.0 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +pytest-benchmark==4.0.0 +python-dateutil==2.8.2 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.10.0 +urllib3==2.2.1 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-elasticsearch diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py index b27d291ba3..8169eb25c4 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py @@ -31,3 +31,9 @@ class Index: dsl_index_span_name = "Elasticsearch/test-index/doc/2" dsl_index_url = "/test-index/doc/2" dsl_search_method = "GET" + +perform_request_mock_path = "elasticsearch.connection.http_urllib3.Urllib3HttpConnection.perform_request" + + +def mock_response(body: str, status_code: int = 200): + return (status_code, {}, body) diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py index b22df18452..377173f7ac 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py @@ -29,3 +29,9 @@ class Index: dsl_index_span_name = "Elasticsearch/test-index/_doc/:id" dsl_index_url = "/test-index/_doc/2" dsl_search_method = "POST" + +perform_request_mock_path = "elasticsearch.connection.http_urllib3.Urllib3HttpConnection.perform_request" + + +def mock_response(body: str, status_code: int = 200): + return (status_code, {}, body) diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es8.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es8.py index 04ed2efda2..a450be68ec 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es8.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es8.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from elastic_transport import ApiResponseMeta, HttpHeaders +from elastic_transport._node import NodeApiResponse from elasticsearch_dsl import Document, Keyword, Text @@ -36,6 +38,23 @@ class Index: } } dsl_index_result = (1, {}, '{"result": "created"}') -dsl_index_span_name = "Elasticsearch/test-index/_doc/2" +dsl_index_span_name = "Elasticsearch/test-index/_doc/:id" dsl_index_url = "/test-index/_doc/2" dsl_search_method = "POST" + +perform_request_mock_path = ( + "elastic_transport._node._http_urllib3.Urllib3HttpNode.perform_request" +) + + +def mock_response(body: str, status_code: int = 200): + return NodeApiResponse( + ApiResponseMeta( + status=status_code, + headers=HttpHeaders({}), + duration=100, + http_version="1.1", + node="node", + ), + body.encode(), + ) diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py index 690cbe3d4c..b0ee170329 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py @@ -51,25 +51,25 @@ def normalize_arguments(doc_type, body=None): - if major_version == 7: - return {"document": body} if body else {} - return ( - {"body": body, "doc_type": doc_type} - if body - else {"doc_type": doc_type} - ) + if major_version < 7: + return ( + {"body": body, "doc_type": doc_type} + if body + else {"doc_type": doc_type} + ) + return {"document": body} if body else {} def get_elasticsearch_client(*args, **kwargs): client = Elasticsearch(*args, **kwargs) - if major_version == 7: + if major_version == 8: + client._verified_elasticsearch = True + elif major_version == 7: client.transport._verified_elasticsearch = True return client -@mock.patch( - "elasticsearch.connection.http_urllib3.Urllib3HttpConnection.perform_request" -) +@mock.patch(helpers.perform_request_mock_path) class TestElasticsearchIntegration(TestBase): search_attributes = { SpanAttributes.DB_SYSTEM: "elasticsearch", @@ -96,7 +96,7 @@ def tearDown(self): ElasticsearchInstrumentor().uninstrument() def test_instrumentor(self, request_mock): - request_mock.return_value = (1, {}, "{}") + request_mock.return_value = helpers.mock_response("{}") es = get_elasticsearch_client(hosts=["http://localhost:9200"]) es.index( @@ -147,7 +147,7 @@ def test_prefix_arg(self, request_mock): prefix = "prefix-from-env" ElasticsearchInstrumentor().uninstrument() ElasticsearchInstrumentor(span_name_prefix=prefix).instrument() - request_mock.return_value = (1, {}, "{}") + request_mock.return_value = helpers.mock_response("{}") self._test_prefix(prefix) def test_prefix_env(self, request_mock): @@ -156,7 +156,7 @@ def test_prefix_env(self, request_mock): os.environ[env_var] = prefix ElasticsearchInstrumentor().uninstrument() ElasticsearchInstrumentor().instrument() - request_mock.return_value = (1, {}, "{}") + request_mock.return_value = helpers.mock_response("{}") del os.environ[env_var] self._test_prefix(prefix) @@ -174,10 +174,8 @@ def _test_prefix(self, prefix): 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}', + request_mock.return_value = helpers.mock_response( + '{"found": false, "timed_out": true, "took": 7}' ) es = get_elasticsearch_client(hosts=["http://localhost:9200"]) es.get( @@ -201,9 +199,18 @@ def test_trace_error_unknown(self, request_mock): 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 + if major_version == 8: + error = {"error": msg} + response = helpers.mock_response( + json.dumps(error), status_code=404 + ) + request_mock.return_value = response + exc = elasticsearch.exceptions.NotFoundError( + msg, meta=response.meta, body=None + ) + else: + exc = elasticsearch.exceptions.NotFoundError(404, msg) + request_mock.side_effect = exc self._test_trace_error(StatusCode.ERROR, exc) def _test_trace_error(self, code, exc): @@ -222,12 +229,13 @@ def _test_trace_error(self, code, exc): span = spans[0] self.assertFalse(span.status.is_ok) self.assertEqual(span.status.status_code, code) + message = getattr(exc, "message", str(exc)) self.assertEqual( - span.status.description, f"{type(exc).__name__}: {exc}" + span.status.description, f"{type(exc).__name__}: {message}" ) def test_parent(self, request_mock): - request_mock.return_value = (1, {}, "{}") + request_mock.return_value = helpers.mock_response("{}") es = get_elasticsearch_client(hosts=["http://localhost:9200"]) with self.tracer.start_as_current_span("parent"): es.index( @@ -245,7 +253,7 @@ def test_parent(self, request_mock): self.assertEqual(child.parent.span_id, parent.context.span_id) def test_multithread(self, request_mock): - request_mock.return_value = (1, {}, "{}") + request_mock.return_value = helpers.mock_response("{}") es = get_elasticsearch_client(hosts=["http://localhost:9200"]) ev = threading.Event() @@ -292,7 +300,9 @@ def target2(): self.assertIsNone(s3.parent) def test_dsl_search(self, request_mock): - request_mock.return_value = (1, {}, '{"hits": {"hits": []}}') + request_mock.return_value = helpers.mock_response( + '{"hits": {"hits": []}}' + ) client = get_elasticsearch_client(hosts=["http://localhost:9200"]) search = Search(using=client, index="test-index").filter( @@ -310,7 +320,9 @@ def test_dsl_search(self, request_mock): ) def test_dsl_search_sanitized(self, request_mock): - request_mock.return_value = (1, {}, '{"hits": {"hits": []}}') + request_mock.return_value = helpers.mock_response( + '{"hits": {"hits": []}}' + ) client = get_elasticsearch_client(hosts=["http://localhost:9200"]) search = Search(using=client, index="test-index").filter( "term", author="testing" @@ -327,7 +339,10 @@ def test_dsl_search_sanitized(self, request_mock): ) def test_dsl_create(self, request_mock): - request_mock.return_value = (1, {}, "{}") + request_mock.side_effect = [ + helpers.mock_response("{}", status_code=404), + helpers.mock_response("{}"), + ] client = get_elasticsearch_client(hosts=["http://localhost:9200"]) Article.init(using=client) @@ -354,7 +369,10 @@ def test_dsl_create(self, request_mock): ) def test_dsl_create_sanitized(self, request_mock): - request_mock.return_value = (1, {}, "{}") + request_mock.side_effect = [ + helpers.mock_response("{}", status_code=404), + helpers.mock_response("{}"), + ] client = get_elasticsearch_client(hosts=["http://localhost:9200"]) Article.init(using=client) @@ -370,7 +388,9 @@ def test_dsl_create_sanitized(self, request_mock): ) def test_dsl_index(self, request_mock): - request_mock.return_value = (1, {}, helpers.dsl_index_result[2]) + request_mock.return_value = helpers.mock_response( + helpers.dsl_index_result[2] + ) client = get_elasticsearch_client(hosts=["http://localhost:9200"]) article = Article( @@ -416,10 +436,8 @@ def request_hook(span, method, url, kwargs): ElasticsearchInstrumentor().uninstrument() ElasticsearchInstrumentor().instrument(request_hook=request_hook) - request_mock.return_value = ( - 1, - {}, - '{"found": false, "timed_out": true, "took": 7}', + request_mock.return_value = helpers.mock_response( + '{"found": false, "timed_out": true, "took": 7}' ) es = get_elasticsearch_client(hosts=["http://localhost:9200"]) index = "test-index" @@ -439,12 +457,26 @@ def request_hook(span, method, url, kwargs): "GET", spans[0].attributes[request_hook_method_attribute] ) expected_url = f"/{index}/_doc/{doc_id}" + if major_version == 8: + expected_url += "?realtime=true&refresh=true" self.assertEqual( expected_url, spans[0].attributes[request_hook_url_attribute], ) - if major_version == 7: + if major_version == 8: + expected_kwargs = { + "body": None, + "request_timeout": "", + "max_retries": "", + "retry_on_status": "", + "retry_on_timeout": "", + "client_meta": "", + "headers": { + "accept": "application/vnd.elasticsearch+json; compatible-with=8" + }, + } + elif major_version == 7: expected_kwargs = { **kwargs, "headers": {"accept": "application/json"}, @@ -452,8 +484,8 @@ def request_hook(span, method, url, kwargs): else: expected_kwargs = {**kwargs} self.assertEqual( - json.dumps(expected_kwargs), - spans[0].attributes[request_hook_kwargs_attribute], + expected_kwargs, + json.loads(spans[0].attributes[request_hook_kwargs_attribute]), ) def test_response_hook(self, request_mock): @@ -492,7 +524,9 @@ def response_hook(span, response): }, } - request_mock.return_value = (1, {}, json.dumps(response_payload)) + request_mock.return_value = helpers.mock_response( + json.dumps(response_payload) + ) es = get_elasticsearch_client(hosts=["http://localhost:9200"]) es.get( index="test-index", **normalize_arguments(doc_type="_doc"), id=1 @@ -512,7 +546,7 @@ def test_no_op_tracer_provider(self, request_mock): tracer_provider=trace.NoOpTracerProvider() ) response_payload = '{"found": false, "timed_out": true, "took": 7}' - request_mock.return_value = (1, {}, response_payload) + request_mock.return_value = helpers.mock_response(response_payload) es = get_elasticsearch_client(hosts=["http://localhost:9200"]) res = es.get( index="test-index", **normalize_arguments(doc_type="_doc"), id=1 @@ -543,7 +577,7 @@ def test_body_sanitization(self, _): ) def test_bulk(self, request_mock): - request_mock.return_value = (1, {}, "{}") + request_mock.return_value = helpers.mock_response("{}") es = get_elasticsearch_client(hosts=["http://localhost:9200"]) es.bulk( diff --git a/tox.ini b/tox.ini index ae11e0f24f..fecc9e5af7 100644 --- a/tox.ini +++ b/tox.ini @@ -92,8 +92,9 @@ envlist = ; below mean these dependencies are being used: ; 0: elasticsearch-dsl==6.4.0 elasticsearch==6.8.2 ; 1: elasticsearch-dsl==7.4.1 elasticsearch==7.17.9 - py3{8,9,10,11}-test-instrumentation-elasticsearch-{0,1} - pypy3-test-instrumentation-elasticsearch-{0,1} + ; 2: elasticsearch-dsl>=8.0,<8.13 elasticsearch>=8.0,<8.13 + py3{8,9,10,11}-test-instrumentation-elasticsearch-{0,1,2} + pypy3-test-instrumentation-elasticsearch-{0,1,2} lint-instrumentation-elasticsearch ; opentelemetry-instrumentation-falcon @@ -716,7 +717,8 @@ commands_pre = elasticsearch: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils elasticsearch-0: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-0.txt elasticsearch-1: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-1.txt - lint-instrumentation-elasticsearch: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-1.txt + elasticsearch-2: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt + lint-instrumentation-elasticsearch: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt asyncio: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api asyncio: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions From d0500c2f8a2160ef21ae4ef3fa3f522f3eea7f94 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 14 May 2024 15:26:31 -0500 Subject: [PATCH 09/20] Add error handling to opentelemetry-bootstrap -a (#2517) * Revert "Refactor bootstrap generation (#2101)" This reverts commit 1ee7261ea7117fbd22e2262e488402213a874125. * Add error handling to opentelemetry-bootstrap -a Fixes #2516 --------- Co-authored-by: Tammy Baylis <96076570+tammy-baylis-swi@users.noreply.github.com> --- .../instrumentation/bootstrap.py | 41 ++++++++++------- .../instrumentation/bootstrap_gen.py | 5 ++ scripts/otel_packaging.py | 46 +++++++------------ 3 files changed, 47 insertions(+), 45 deletions(-) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index 0c8f0aa3c4..6f86a539b2 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -14,8 +14,14 @@ import argparse import logging -import subprocess import sys +from subprocess import ( + PIPE, + CalledProcessError, + Popen, + SubprocessError, + check_call, +) import pkg_resources @@ -34,7 +40,7 @@ def wrapper(package=None): if package: return func(package) return func() - except subprocess.SubprocessError as exp: + except SubprocessError as exp: cmd = getattr(exp, "cmd", None) if cmd: msg = f'Error calling system command "{" ".join(cmd)}"' @@ -48,18 +54,21 @@ def wrapper(package=None): @_syscall def _sys_pip_install(package): # explicit upgrade strategy to override potential pip config - subprocess.check_call( - [ - sys.executable, - "-m", - "pip", - "install", - "-U", - "--upgrade-strategy", - "only-if-needed", - package, - ] - ) + try: + check_call( + [ + sys.executable, + "-m", + "pip", + "install", + "-U", + "--upgrade-strategy", + "only-if-needed", + package, + ] + ) + except CalledProcessError as error: + print(error) def _pip_check(): @@ -70,8 +79,8 @@ def _pip_check(): 'opentelemetry-instrumentation-flask 1.0.1 has requirement opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.' To not be too restrictive, we'll only check for relevant packages. """ - with subprocess.Popen( - [sys.executable, "-m", "pip", "check"], stdout=subprocess.PIPE + with Popen( + [sys.executable, "-m", "pip", "check"], stdout=PIPE ) as check_pipe: pip_check = check_pipe.communicate()[0].decode() pip_check_lower = pip_check.lower() diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index 55d2f498a1..9eebd5bb38 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -24,6 +24,10 @@ "library": "aiohttp ~= 3.0", "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.46b0.dev", }, + { + "library": "aiohttp ~= 3.0", + "instrumentation": "opentelemetry-instrumentation-aiohttp-server==0.46b0.dev", + }, { "library": "aiopg >= 0.13.0, < 2.0.0", "instrumentation": "opentelemetry-instrumentation-aiopg==0.46b0.dev", @@ -187,6 +191,7 @@ "opentelemetry-instrumentation-dbapi==0.46b0.dev", "opentelemetry-instrumentation-logging==0.46b0.dev", "opentelemetry-instrumentation-sqlite3==0.46b0.dev", + "opentelemetry-instrumentation-threading==0.46b0.dev", "opentelemetry-instrumentation-urllib==0.46b0.dev", "opentelemetry-instrumentation-wsgi==0.46b0.dev", ] diff --git a/scripts/otel_packaging.py b/scripts/otel_packaging.py index c6c11c45fa..2f42e44189 100644 --- a/scripts/otel_packaging.py +++ b/scripts/otel_packaging.py @@ -12,55 +12,43 @@ # See the License for the specific language governing permissions and # limitations under the License. -from tomli import load -from os import path, listdir -from subprocess import check_output, CalledProcessError -from requests import get +import os +import subprocess +from subprocess import CalledProcessError -scripts_path = path.dirname(path.abspath(__file__)) -root_path = path.dirname(scripts_path) -instrumentations_path = path.join(root_path, "instrumentation") +import tomli + +scripts_path = os.path.dirname(os.path.abspath(__file__)) +root_path = os.path.dirname(scripts_path) +instrumentations_path = os.path.join(root_path, "instrumentation") def get_instrumentation_packages(): - for pkg in sorted(listdir(instrumentations_path)): - pkg_path = path.join(instrumentations_path, pkg) - if not path.isdir(pkg_path): + for pkg in sorted(os.listdir(instrumentations_path)): + pkg_path = os.path.join(instrumentations_path, pkg) + if not os.path.isdir(pkg_path): continue - error = f"Could not get version for package {pkg}" - try: - hatch_version = check_output( + version = subprocess.check_output( "hatch version", shell=True, cwd=pkg_path, - universal_newlines=True + universal_newlines=True, ) - except CalledProcessError as exc: print(f"Could not get hatch version from path {pkg_path}") print(exc.output) + raise exc - try: - response = get(f"https://pypi.org/pypi/{pkg}/json", timeout=10) - - except Exception: - print(error) - continue - - if response.status_code != 200: - print(error) - continue - - pyproject_toml_path = path.join(pkg_path, "pyproject.toml") + pyproject_toml_path = os.path.join(pkg_path, "pyproject.toml") with open(pyproject_toml_path, "rb") as file: - pyproject_toml = load(file) + pyproject_toml = tomli.load(file) instrumentation = { "name": pyproject_toml["project"]["name"], - "version": hatch_version.strip(), + "version": version.strip(), "instruments": pyproject_toml["project"]["optional-dependencies"][ "instruments" ], From 460fc335836c395db8472ecf464e7ecd94c08925 Mon Sep 17 00:00:00 2001 From: Guillermo Date: Wed, 15 May 2024 03:52:59 -0600 Subject: [PATCH 10/20] Fix typo in sample code (#2494) Co-authored-by: Riccardo Magliocchetti --- .../src/opentelemetry/instrumentation/tornado/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py index 5c99457a39..be9129bda0 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py @@ -87,14 +87,14 @@ def client_request_hook(span, request): # will be called after a outgoing request made with # `tornado.httpclient.AsyncHTTPClient.fetch` finishes. # `response`` is an instance of ``Future[tornado.httpclient.HTTPResponse]`. - def client_resposne_hook(span, future): + def client_response_hook(span, future): pass # apply tornado instrumentation with hooks TornadoInstrumentor().instrument( server_request_hook=server_request_hook, client_request_hook=client_request_hook, - client_response_hook=client_resposne_hook + client_response_hook=client_response_hook ) Capture HTTP request and response headers From f8758c6902ed725864ff677f739829aa6bce2078 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 16 May 2024 14:05:21 -0700 Subject: [PATCH 11/20] Implement functions resource detector (#2523) * Update .pylintrc * fn * Update CHANGELOG.md * commments * Add deployment.environment to functions detector * Revert "Add deployment.environment to functions detector" This reverts commit 5411759711b8bc9976705deb416d5ffd8f65590f. * Remove deployment.environment from readme * Release 0.1.5 --------- Co-authored-by: jeremydvoss --- .../CHANGELOG.md | 4 +- .../README.rst | 12 +- .../pyproject.toml | 1 + .../resource/detector/azure/__init__.py | 2 + .../resource/detector/azure/_constants.py | 6 + .../resource/detector/azure/_utils.py | 27 ++++- .../resource/detector/azure/app_service.py | 32 ++--- .../resource/detector/azure/functions.py | 68 +++++++++++ .../resource/detector/azure/version.py | 2 +- .../tests/test_app_service.py | 39 +++++++ .../tests/test_functions.py | 110 ++++++++++++++++++ 11 files changed, 274 insertions(+), 29 deletions(-) create mode 100644 resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/functions.py create mode 100644 resource/opentelemetry-resource-detector-azure/tests/test_functions.py diff --git a/resource/opentelemetry-resource-detector-azure/CHANGELOG.md b/resource/opentelemetry-resource-detector-azure/CHANGELOG.md index 8954fc5359..f77fce18f1 100644 --- a/resource/opentelemetry-resource-detector-azure/CHANGELOG.md +++ b/resource/opentelemetry-resource-detector-azure/CHANGELOG.md @@ -5,10 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## Version 0.1.5 (2024-05-16) - Ignore vm detector if already in other rps ([#2456](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2456)) +- Implement functions resource detector + ([#2523](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2523)) ## Version 0.1.4 (2024-04-05) diff --git a/resource/opentelemetry-resource-detector-azure/README.rst b/resource/opentelemetry-resource-detector-azure/README.rst index 6a376534ad..baf2dddbbe 100644 --- a/resource/opentelemetry-resource-detector-azure/README.rst +++ b/resource/opentelemetry-resource-detector-azure/README.rst @@ -60,7 +60,17 @@ The Azure App Service Resource Detector sets the following Resource Attributes: * ``service.instance.id`` set to the value of the ``WEBSITE_INSTANCE_ID`` environment variable. * ``azure.app.service.stamp`` set to the value of the ``WEBSITE_HOME_STAMPNAME`` environment variable. -The Azure VM Resource Detector sets the following Resource Attributes according to the response from the `Azure Metadata Service `_: + The Azure Functions Resource Detector sets the following Resource Attributes: + * ``service.name`` set to the value of the ``WEBSITE_SITE_NAME`` environment variable. + * ``process.id`` set to the process ID collected from the running process. + * ``cloud.platform`` set to ``azure_functions``. + * ``cloud.provider`` set to ``azure``. + * ``cloud.resource_id`` set using the ``WEBSITE_RESOURCE_GROUP``, ``WEBSITE_OWNER_NAME``, and ``WEBSITE_SITE_NAME`` environment variables. + * ``cloud.region`` set to the value of the ``REGION_NAME`` environment variable. + * ``faas.instance`` set to the value of the ``WEBSITE_INSTANCE_ID`` environment variable. + * ``faas.max_memory`` set to the value of the ``WEBSITE_MEMORY_LIMIT_MB`` environment variable. + +The Azure VM Resource Detector sets the following Resource Attributes according to the response from the `Azure Metadata Service `_: * ``azure.vm.scaleset.name`` set to the value of the ``vmScaleSetName`` field. * ``azure.vm.sku`` set to the value of the ``sku`` field. * ``cloud.platform`` set to the value of the ``azure_vm``. diff --git a/resource/opentelemetry-resource-detector-azure/pyproject.toml b/resource/opentelemetry-resource-detector-azure/pyproject.toml index 72260709f9..efa1b24ee7 100644 --- a/resource/opentelemetry-resource-detector-azure/pyproject.toml +++ b/resource/opentelemetry-resource-detector-azure/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ [project.entry-points.opentelemetry_resource_detector] azure_app_service = "opentelemetry.resource.detector.azure.app_service:AzureAppServiceResourceDetector" +azure_functions = "opentelemetry.resource.detector.azure.functions:AzureFunctionsResourceDetector" azure_vm = "opentelemetry.resource.detector.azure.vm:AzureVMResourceDetector" [project.urls] diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/__init__.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/__init__.py index 913b677c3e..628a8ab781 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/__init__.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/__init__.py @@ -15,11 +15,13 @@ # pylint: disable=import-error from .app_service import AzureAppServiceResourceDetector +from .functions import AzureFunctionsResourceDetector from .version import __version__ from .vm import AzureVMResourceDetector __all__ = [ "AzureAppServiceResourceDetector", + "AzureFunctionsResourceDetector", "AzureVMResourceDetector", "__version__", ] diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_constants.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_constants.py index dddc6632ac..3a6415e0d5 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_constants.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_constants.py @@ -43,6 +43,12 @@ # Functions _FUNCTIONS_WORKER_RUNTIME = "FUNCTIONS_WORKER_RUNTIME" +_WEBSITE_MEMORY_LIMIT_MB = "WEBSITE_MEMORY_LIMIT_MB" + +_FUNCTIONS_ATTRIBUTE_ENV_VARS = { + ResourceAttributes.FAAS_INSTANCE: _WEBSITE_INSTANCE_ID, + ResourceAttributes.FAAS_MAX_MEMORY: _WEBSITE_MEMORY_LIMIT_MB, +} # Vm diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_utils.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_utils.py index 3f73613945..62d00c5a6c 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_utils.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_utils.py @@ -11,27 +11,44 @@ # 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 +from os import environ +from typing import Optional from ._constants import ( _AKS_ARM_NAMESPACE_ID, _FUNCTIONS_WORKER_RUNTIME, + _WEBSITE_OWNER_NAME, + _WEBSITE_RESOURCE_GROUP, _WEBSITE_SITE_NAME, ) def _is_on_aks() -> bool: - return os.environ.get(_AKS_ARM_NAMESPACE_ID) is not None + return environ.get(_AKS_ARM_NAMESPACE_ID) is not None def _is_on_app_service() -> bool: - return os.environ.get(_WEBSITE_SITE_NAME) is not None + return environ.get(_WEBSITE_SITE_NAME) is not None def _is_on_functions() -> bool: - return os.environ.get(_FUNCTIONS_WORKER_RUNTIME) is not None + return environ.get(_FUNCTIONS_WORKER_RUNTIME) is not None def _can_ignore_vm_detect() -> bool: return _is_on_aks() or _is_on_app_service() or _is_on_functions() + + +def _get_azure_resource_uri() -> Optional[str]: + website_site_name = environ.get(_WEBSITE_SITE_NAME) + website_resource_group = environ.get(_WEBSITE_RESOURCE_GROUP) + website_owner_name = environ.get(_WEBSITE_OWNER_NAME) + + subscription_id = website_owner_name + if website_owner_name and "+" in website_owner_name: + subscription_id = website_owner_name[0 : website_owner_name.index("+")] + + if not (website_site_name and website_resource_group and subscription_id): + return None + + return f"/subscriptions/{subscription_id}/resourceGroups/{website_resource_group}/providers/Microsoft.Web/sites/{website_site_name}" diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py index 613d8f9410..41371b8eec 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional from os import environ from opentelemetry.sdk.resources import Resource, ResourceDetector @@ -20,29 +21,32 @@ CloudProviderValues, ResourceAttributes, ) +from opentelemetry.resource.detector.azure._utils import _get_azure_resource_uri from ._constants import ( _APP_SERVICE_ATTRIBUTE_ENV_VARS, - _WEBSITE_OWNER_NAME, - _WEBSITE_RESOURCE_GROUP, _WEBSITE_SITE_NAME, ) +from opentelemetry.resource.detector.azure._utils import _is_on_functions + class AzureAppServiceResourceDetector(ResourceDetector): def detect(self) -> Resource: attributes = {} website_site_name = environ.get(_WEBSITE_SITE_NAME) if website_site_name: - attributes[ResourceAttributes.SERVICE_NAME] = website_site_name + # Functions resource detector takes priority with `service.name` and `cloud.platform` + if not _is_on_functions(): + attributes[ResourceAttributes.SERVICE_NAME] = website_site_name + attributes[ResourceAttributes.CLOUD_PLATFORM] = ( + CloudPlatformValues.AZURE_APP_SERVICE.value + ) attributes[ResourceAttributes.CLOUD_PROVIDER] = ( CloudProviderValues.AZURE.value ) - attributes[ResourceAttributes.CLOUD_PLATFORM] = ( - CloudPlatformValues.AZURE_APP_SERVICE.value - ) - azure_resource_uri = _get_azure_resource_uri(website_site_name) + azure_resource_uri = _get_azure_resource_uri() if azure_resource_uri: attributes[ResourceAttributes.CLOUD_RESOURCE_ID] = ( azure_resource_uri @@ -53,17 +57,3 @@ def detect(self) -> Resource: attributes[key] = value return Resource(attributes) - - -def _get_azure_resource_uri(website_site_name): - website_resource_group = environ.get(_WEBSITE_RESOURCE_GROUP) - website_owner_name = environ.get(_WEBSITE_OWNER_NAME) - - subscription_id = website_owner_name - if website_owner_name and "+" in website_owner_name: - subscription_id = website_owner_name[0 : website_owner_name.index("+")] - - if not (website_resource_group and subscription_id): - return None - - return f"/subscriptions/{subscription_id}/resourceGroups/{website_resource_group}/providers/Microsoft.Web/sites/{website_site_name}" diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/functions.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/functions.py new file mode 100644 index 0000000000..0bf9a10f86 --- /dev/null +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/functions.py @@ -0,0 +1,68 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os import environ, getpid + +from opentelemetry.sdk.resources import Resource, ResourceDetector +from opentelemetry.semconv.resource import ( + CloudPlatformValues, + CloudProviderValues, + ResourceAttributes, +) + +from ._constants import ( + _FUNCTIONS_ATTRIBUTE_ENV_VARS, + _REGION_NAME, + _WEBSITE_SITE_NAME, +) +from opentelemetry.resource.detector.azure._utils import ( + _get_azure_resource_uri, + _is_on_functions, +) + + +class AzureFunctionsResourceDetector(ResourceDetector): + def detect(self) -> Resource: + attributes = {} + if _is_on_functions(): + website_site_name = environ.get(_WEBSITE_SITE_NAME) + if website_site_name: + attributes[ResourceAttributes.SERVICE_NAME] = website_site_name + attributes[ResourceAttributes.PROCESS_PID] = getpid() + attributes[ResourceAttributes.CLOUD_PROVIDER] = ( + CloudProviderValues.AZURE.value + ) + attributes[ResourceAttributes.CLOUD_PLATFORM] = ( + CloudPlatformValues.AZURE_FUNCTIONS.value + ) + cloud_region = environ.get(_REGION_NAME) + if cloud_region: + attributes[ResourceAttributes.CLOUD_REGION] = cloud_region + azure_resource_uri = _get_azure_resource_uri() + if azure_resource_uri: + attributes[ResourceAttributes.CLOUD_RESOURCE_ID] = ( + azure_resource_uri + ) + for key, env_var in _FUNCTIONS_ATTRIBUTE_ENV_VARS.items(): + value = environ.get(env_var) + if value: + if key == ResourceAttributes.FAAS_MAX_MEMORY: + try: + value = int(value) + except ValueError: + continue + attributes[key] = value + + return Resource(attributes) + diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/version.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/version.py index f961659f70..fac29d773f 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/version.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.4" +__version__ = "0.1.5" diff --git a/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py b/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py index c5d2396dab..6c3d395994 100644 --- a/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py +++ b/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py @@ -68,6 +68,45 @@ def test_on_app_service(self): self.assertEqual( attributes["azure.app.service.stamp"], TEST_WEBSITE_HOME_STAMPNAME ) + + @patch.dict( + "os.environ", + { + "FUNCTIONS_WORKER_RUNTIME": "1", + "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, + "REGION_NAME": TEST_REGION_NAME, + "WEBSITE_SLOT_NAME": TEST_WEBSITE_SLOT_NAME, + "WEBSITE_HOSTNAME": TEST_WEBSITE_HOSTNAME, + "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, + "WEBSITE_HOME_STAMPNAME": TEST_WEBSITE_HOME_STAMPNAME, + "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, + "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, + }, + clear=True, + ) + def test_on_app_service_with_functions(self): + resource = AzureAppServiceResourceDetector().detect() + attributes = resource.attributes + self.assertIsNone(attributes.get("service.name")) + self.assertEqual(attributes["cloud.provider"], "azure") + self.assertIsNone(attributes.get("cloud.platform")) + + self.assertEqual( + attributes["cloud.resource_id"], + f"/subscriptions/{TEST_WEBSITE_OWNER_NAME}/resourceGroups/{TEST_WEBSITE_RESOURCE_GROUP}/providers/Microsoft.Web/sites/{TEST_WEBSITE_SITE_NAME}", + ) + + self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME) + self.assertEqual( + attributes["deployment.environment"], TEST_WEBSITE_SLOT_NAME + ) + self.assertEqual(attributes["host.id"], TEST_WEBSITE_HOSTNAME) + self.assertEqual( + attributes["service.instance.id"], TEST_WEBSITE_INSTANCE_ID + ) + self.assertEqual( + attributes["azure.app.service.stamp"], TEST_WEBSITE_HOME_STAMPNAME + ) @patch.dict( "os.environ", diff --git a/resource/opentelemetry-resource-detector-azure/tests/test_functions.py b/resource/opentelemetry-resource-detector-azure/tests/test_functions.py new file mode 100644 index 0000000000..1f5354c500 --- /dev/null +++ b/resource/opentelemetry-resource-detector-azure/tests/test_functions.py @@ -0,0 +1,110 @@ +# 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 unittest +from unittest.mock import patch + +# pylint: disable=no-name-in-module +from opentelemetry.resource.detector.azure.functions import ( + AzureFunctionsResourceDetector, +) + +TEST_WEBSITE_SITE_NAME = "TEST_WEBSITE_SITE_NAME" +TEST_REGION_NAME = "TEST_REGION_NAME" +TEST_WEBSITE_INSTANCE_ID = "TEST_WEBSITE_INSTANCE_ID" + +TEST_WEBSITE_RESOURCE_GROUP = "TEST_WEBSITE_RESOURCE_GROUP" +TEST_WEBSITE_OWNER_NAME = "TEST_WEBSITE_OWNER_NAME" +TEST_WEBSITE_MEMORY_LIMIT_MB = "1024" + + +class TestAzureAppServiceResourceDetector(unittest.TestCase): + @patch.dict( + "os.environ", + { + "FUNCTIONS_WORKER_RUNTIME": "1", + "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, + "REGION_NAME": TEST_REGION_NAME, + "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, + "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, + "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, + "WEBSITE_MEMORY_LIMIT_MB": TEST_WEBSITE_MEMORY_LIMIT_MB, + }, + clear=True, + ) + @patch("opentelemetry.resource.detector.azure.functions.getpid") + def test_on_functions(self, pid_mock): + pid_mock.return_value = 1000 + resource = AzureFunctionsResourceDetector().detect() + attributes = resource.attributes + self.assertEqual(attributes["service.name"], TEST_WEBSITE_SITE_NAME) + self.assertEqual(attributes["cloud.provider"], "azure") + self.assertEqual(attributes["cloud.platform"], "azure_functions") + self.assertEqual(attributes["process.pid"], 1000) + + self.assertEqual( + attributes["cloud.resource_id"], + f"/subscriptions/{TEST_WEBSITE_OWNER_NAME}/resourceGroups/{TEST_WEBSITE_RESOURCE_GROUP}/providers/Microsoft.Web/sites/{TEST_WEBSITE_SITE_NAME}", + ) + + self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME) + self.assertEqual(attributes["faas.instance"], TEST_WEBSITE_INSTANCE_ID) + self.assertEqual(attributes["faas.max_memory"], 1024) + + @patch.dict( + "os.environ", + { + "FUNCTIONS_WORKER_RUNTIME": "1", + "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, + "REGION_NAME": TEST_REGION_NAME, + "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, + "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, + "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, + "WEBSITE_MEMORY_LIMIT_MB": "error", + }, + clear=True, + ) + @patch("opentelemetry.resource.detector.azure.functions.getpid") + def test_on_functions_error_memory(self, pid_mock): + pid_mock.return_value = 1000 + resource = AzureFunctionsResourceDetector().detect() + attributes = resource.attributes + self.assertEqual(attributes["service.name"], TEST_WEBSITE_SITE_NAME) + self.assertEqual(attributes["cloud.provider"], "azure") + self.assertEqual(attributes["cloud.platform"], "azure_functions") + self.assertEqual(attributes["process.pid"], 1000) + + self.assertEqual( + attributes["cloud.resource_id"], + f"/subscriptions/{TEST_WEBSITE_OWNER_NAME}/resourceGroups/{TEST_WEBSITE_RESOURCE_GROUP}/providers/Microsoft.Web/sites/{TEST_WEBSITE_SITE_NAME}", + ) + + self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME) + self.assertEqual(attributes["faas.instance"], TEST_WEBSITE_INSTANCE_ID) + self.assertIsNone(attributes.get("faas.max_memory")) + + @patch.dict( + "os.environ", + { + "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, + "REGION_NAME": TEST_REGION_NAME, + "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, + "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, + "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, + "WEBSITE_MEMORY_LIMIT_MB": TEST_WEBSITE_MEMORY_LIMIT_MB, + }, + clear=True, + ) + def test_off_app_service(self): + resource = AzureFunctionsResourceDetector().detect() + self.assertEqual(resource.attributes, {}) From f4f3042f85fa68565c4da3f96d437b15d78e1063 Mon Sep 17 00:00:00 2001 From: Povilas Versockas Date: Wed, 22 May 2024 07:48:54 +0300 Subject: [PATCH 12/20] fix(async-io): check for __name__ atribute when tracing coroutine (#2521) --- CHANGELOG.md | 2 + .../instrumentation/asyncio/__init__.py | 2 + .../tests/test_asyncio_anext.py | 56 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_anext.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d10983c10b..b963cfddc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2474](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2474)) - `opentelemetry-instrumentation-elasticsearch` Improved support for version 8 ([#2420](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2420)) +- `opentelemetry-instrumentation-asyncio` Check for __name__ attribute in the coroutine + ([#2521](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2521)) ## Version 1.24.0/0.45b0 (2024-03-28) diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py index 72aa5fd2aa..6d82da6cd0 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py @@ -261,6 +261,8 @@ def trace_item(self, coro_or_future): return coro_or_future async def trace_coroutine(self, coro): + if not hasattr(coro, "__name__"): + return start = default_timer() attr = { "type": "coroutine", diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_anext.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_anext.py new file mode 100644 index 0000000000..9ce3fc4b33 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_anext.py @@ -0,0 +1,56 @@ +# 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 asyncio +from unittest.mock import patch + +# pylint: disable=no-name-in-module +from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor +from opentelemetry.instrumentation.asyncio.environment_variables import ( + OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE, +) +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import get_tracer + + +class TestAsyncioAnext(TestBase): + @patch.dict( + "os.environ", + {OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "async_func"}, + ) + def setUp(self): + super().setUp() + AsyncioInstrumentor().instrument() + self._tracer = get_tracer( + __name__, + ) + + def tearDown(self): + super().tearDown() + AsyncioInstrumentor().uninstrument() + + # Asyncio anext() does not have __name__ attribute, which is used to determine if the coroutine should be traced. + # This test is to ensure that the instrumentation does not break when the coroutine does not have __name__ attribute. + def test_asyncio_anext(self): + async def main(): + async def async_gen(): + for it in range(2): + yield it + + async_gen_instance = async_gen() + agen = anext(async_gen_instance) + await asyncio.create_task(agen) + + asyncio.run(main()) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) From c28f9b837f386cffd9b0f8800f7653a85ca5c60e Mon Sep 17 00:00:00 2001 From: Jeremy Voss Date: Wed, 22 May 2024 10:45:10 -0700 Subject: [PATCH 13/20] Update functions detector readme (#2533) --- .../README.rst | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/resource/opentelemetry-resource-detector-azure/README.rst b/resource/opentelemetry-resource-detector-azure/README.rst index baf2dddbbe..19a0f97f32 100644 --- a/resource/opentelemetry-resource-detector-azure/README.rst +++ b/resource/opentelemetry-resource-detector-azure/README.rst @@ -9,6 +9,7 @@ OpenTelemetry Resource detectors for Azure This library contains OpenTelemetry `Resource Detectors `_ for the following Azure resources: * `Azure App Service `_ * `Azure Virtual Machines `_ + * `Azure Functions (Experimental) `_ Installation ------------ @@ -60,16 +61,6 @@ The Azure App Service Resource Detector sets the following Resource Attributes: * ``service.instance.id`` set to the value of the ``WEBSITE_INSTANCE_ID`` environment variable. * ``azure.app.service.stamp`` set to the value of the ``WEBSITE_HOME_STAMPNAME`` environment variable. - The Azure Functions Resource Detector sets the following Resource Attributes: - * ``service.name`` set to the value of the ``WEBSITE_SITE_NAME`` environment variable. - * ``process.id`` set to the process ID collected from the running process. - * ``cloud.platform`` set to ``azure_functions``. - * ``cloud.provider`` set to ``azure``. - * ``cloud.resource_id`` set using the ``WEBSITE_RESOURCE_GROUP``, ``WEBSITE_OWNER_NAME``, and ``WEBSITE_SITE_NAME`` environment variables. - * ``cloud.region`` set to the value of the ``REGION_NAME`` environment variable. - * ``faas.instance`` set to the value of the ``WEBSITE_INSTANCE_ID`` environment variable. - * ``faas.max_memory`` set to the value of the ``WEBSITE_MEMORY_LIMIT_MB`` environment variable. - The Azure VM Resource Detector sets the following Resource Attributes according to the response from the `Azure Metadata Service `_: * ``azure.vm.scaleset.name`` set to the value of the ``vmScaleSetName`` field. * ``azure.vm.sku`` set to the value of the ``sku`` field. @@ -84,6 +75,16 @@ The Azure VM Resource Detector sets the following Resource Attributes according * ``os.version`` set to the value of the ``version`` field. * ``service.instance.id`` set to the value of the ``vmId`` field. +The Azure Functions Resource Detector is currently experimental. It sets the following Resource Attributes: + * ``service.name`` set to the value of the ``WEBSITE_SITE_NAME`` environment variable. + * ``process.id`` set to the process ID collected from the running process. + * ``cloud.platform`` set to ``azure_functions``. + * ``cloud.provider`` set to ``azure``. + * ``cloud.resource_id`` set using the ``WEBSITE_RESOURCE_GROUP``, ``WEBSITE_OWNER_NAME``, and ``WEBSITE_SITE_NAME`` environment variables. + * ``cloud.region`` set to the value of the ``REGION_NAME`` environment variable. + * ``faas.instance`` set to the value of the ``WEBSITE_INSTANCE_ID`` environment variable. + * ``faas.max_memory`` set to the value of the ``WEBSITE_MEMORY_LIMIT_MB`` environment variable. + For more information, see the `Semantic Conventions for Cloud Resource Attributes `_. References From da75015fade9f8adf58416fdeb8684a4a263d7b8 Mon Sep 17 00:00:00 2001 From: Adin Hodovic Date: Thu, 23 May 2024 11:02:46 +0200 Subject: [PATCH 14/20] fix: Ensure compability with Psycopg3 to extract libpq build version (#2500) * fix: Ensure compability with Psycopg3 to extract libpq build version Struggling with getting dbapi and psycopg3 working. Think this is the error, __libpq_version does not exist on psycopg3 https://github.com/psycopg/psycopg/blob/master/psycopg/psycopg/pq/pq_ctypes.py#L1220 * docs: Add changelog entry * docs: Fix spelling --------- Co-authored-by: Riccardo Magliocchetti Co-authored-by: Shalev Roda <65566801+shalevr@users.noreply.github.com> --- CHANGELOG.md | 4 +-- .../instrumentation/dbapi/__init__.py | 9 ++++++- .../tests/test_dbapi_integration.py | 26 +++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b963cfddc2..038706f35c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- `opentelemetry-instrumentation-dbapi` Fix compatibility with Psycopg3 to extract libpq build version (#2500)[https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2500] - `opentelemetry-instrumentation-grpc` AioClientInterceptor should propagate with a Metadata object ([#2363](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2363)) - `opentelemetry-instrumentation-boto3sqs` Instrument Session and resource @@ -126,7 +127,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1959](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1959)) - `opentelemetry-resource-detector-azure` Added dependency for Cloud Resource ID attribute ([#2072](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2072)) - + ## Version 1.21.0/0.42b0 (2023-11-01) ### Added @@ -1540,4 +1541,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-resource-detector-azure` Suppress instrumentation for `urllib` call ([#2178](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2178)) - AwsLambdaInstrumentor handles and re-raises function exception ([#2245](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2245)) - diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py index b0acbed185..0857d2989b 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py @@ -427,12 +427,19 @@ def traced_execution( if args and self._commenter_enabled: try: args_list = list(args) + if hasattr(self._connect_module, "__libpq_version__"): + libpq_version = self._connect_module.__libpq_version__ + else: + libpq_version = ( + self._connect_module.pq.__build_version__ + ) + commenter_data = { # Psycopg2/framework information "db_driver": f"psycopg2:{self._connect_module.__version__.split(' ')[0]}", "dbapi_threadsafety": self._connect_module.threadsafety, "dbapi_level": self._connect_module.apilevel, - "libpq_version": self._connect_module.__libpq_version__, + "libpq_version": libpq_version, "driver_paramstyle": self._connect_module.paramstyle, } if self._commenter_options.get( diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py index 0d19ce8373..d0835e93e6 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py @@ -275,6 +275,32 @@ def test_executemany_comment(self): 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}'\*/;", ) + def test_compatible_build_version_psycopg_psycopg2_libpq(self): + connect_module = mock.MagicMock() + connect_module.__version__ = mock.MagicMock() + connect_module.pq = mock.MagicMock() + connect_module.pq.__build_version__ = 123 + connect_module.apilevel = 123 + connect_module.threadsafety = 123 + connect_module.paramstyle = "test" + + db_integration = dbapi.DatabaseApiIntegration( + "testname", + "testcomponent", + enable_commenter=True, + commenter_options={"db_driver": False, "dbapi_level": False}, + connect_module=connect_module, + ) + mock_connection = db_integration.wrapped_connection( + mock_connect, {}, {} + ) + cursor = mock_connection.cursor() + cursor.executemany("Select 1;") + self.assertRegex( + cursor.query, + 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}'\*/;", + ) + def test_executemany_flask_integration_comment(self): connect_module = mock.MagicMock() connect_module.__version__ = mock.MagicMock() From 66a107fa49fc6d264ae6d690c09c9f04bc4a5dd7 Mon Sep 17 00:00:00 2001 From: Povilas Versockas Date: Fri, 24 May 2024 02:28:50 +0300 Subject: [PATCH 15/20] fix(async-io): return coro when __name__ is not present (#2541) --- .../src/opentelemetry/instrumentation/asyncio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py index 6d82da6cd0..fc1b535270 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py @@ -262,7 +262,7 @@ def trace_item(self, coro_or_future): async def trace_coroutine(self, coro): if not hasattr(coro, "__name__"): - return + return coro start = default_timer() attr = { "type": "coroutine", From 78285a5795db18e25126f1452a92a536a611b1be Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 24 May 2024 10:49:18 -0700 Subject: [PATCH 16/20] Pin codespell version to fix builds (#2550) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index fecc9e5af7..0f1e8badcc 100644 --- a/tox.ini +++ b/tox.ini @@ -1206,7 +1206,7 @@ commands = basepython: python3 recreate = True deps = - codespell + codespell==2.2.6 commands = codespell From c1a51fde96c0f53ca185c3816be9fe69cba4f528 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 24 May 2024 19:57:29 +0200 Subject: [PATCH 17/20] Pre Python 3.12 enablement fixes (#2529) --- .../test-requirements.txt | 2 +- .../tests/test_botocore_dynamodb.py | 12 ++++++------ .../tests/test_utils.py | 2 +- tox.ini | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt index 9dcf9f9c0d..7c11b1e063 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt @@ -28,7 +28,7 @@ jsonschema==4.21.1 jsonschema-specifications==2023.12.1 junit-xml==1.9 MarkupSafe==2.0.1 -moto==2.2.20 +moto==3.1.19 mpmath==1.3.0 networkx==3.1 packaging==23.2 diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_dynamodb.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_dynamodb.py index 1b7f5bb0cb..12ebe8f2b7 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_dynamodb.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_dynamodb.py @@ -324,14 +324,14 @@ def test_get_item(self): Key={"id": {"S": "1"}}, ConsistentRead=True, AttributesToGet=["id"], - ProjectionExpression="1,2", + ProjectionExpression="PE", ReturnConsumedCapacity="TOTAL", ) span = self.assert_span("GetItem") self.assert_table_names(span, self.default_table_name) self.assert_consistent_read(span, True) - self.assert_projection(span, "1,2") + self.assert_projection(span, "PE") self.assert_consumed_capacity(span, self.default_table_name) @mock_dynamodb2 @@ -390,7 +390,7 @@ def test_query(self): } }, ScanIndexForward=True, - ProjectionExpression="1,2", + ProjectionExpression="PE", ReturnConsumedCapacity="TOTAL", ) @@ -403,7 +403,7 @@ def test_query(self): self.assert_consistent_read(span, True) self.assert_index_name(span, "lsi") self.assert_limit(span, 42) - self.assert_projection(span, "1,2") + self.assert_projection(span, "PE") self.assert_select(span, "ALL_ATTRIBUTES") self.assert_consumed_capacity(span, self.default_table_name) @@ -419,7 +419,7 @@ def test_scan(self): Select="ALL_ATTRIBUTES", TotalSegments=17, Segment=21, - ProjectionExpression="1,2", + ProjectionExpression="PE", ConsistentRead=True, ReturnConsumedCapacity="TOTAL", ) @@ -440,7 +440,7 @@ def test_scan(self): self.assert_consistent_read(span, True) self.assert_index_name(span, "lsi") self.assert_limit(span, 42) - self.assert_projection(span, "1,2") + self.assert_projection(span, "PE") self.assert_select(span, "ALL_ATTRIBUTES") self.assert_consumed_capacity(span, self.default_table_name) diff --git a/instrumentation/opentelemetry-instrumentation-pika/tests/test_utils.py b/instrumentation/opentelemetry-instrumentation-pika/tests/test_utils.py index d651ea64c9..7c0aa7a715 100644 --- a/instrumentation/opentelemetry-instrumentation-pika/tests/test_utils.py +++ b/instrumentation/opentelemetry-instrumentation-pika/tests/test_utils.py @@ -558,7 +558,7 @@ def test_decorate_deque_proxy( self.assertEqual(res, evt) generator_info.pending_events.popleft.assert_called_once() extract.assert_not_called() - context_get_current.not_called() + context_get_current.assert_not_called() context_detach.assert_called_once() context_attach.assert_not_called() get_span.assert_not_called() diff --git a/tox.ini b/tox.ini index 0f1e8badcc..d6fb9eee10 100644 --- a/tox.ini +++ b/tox.ini @@ -1198,8 +1198,8 @@ deps = -r dev-requirements.txt commands = - black --config {toxinidir}/pyproject.toml {{toxinidir}} --diff --check - isort --settings-path {toxinidir}/.isort.cfg {{toxinidir}} --diff --check-only + black --config {toxinidir}/pyproject.toml {toxinidir} --diff --check + isort --settings-path {toxinidir}/.isort.cfg {toxinidir} --diff --check-only flake8 --config {toxinidir}/.flake8 {toxinidir} [testenv:spellcheck] From e6409568c11f5ec1341e85770f2f01dded676d7a Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 24 May 2024 20:12:53 +0200 Subject: [PATCH 18/20] Reenable pylint broad exception (#2536) --- .pylintrc | 2 +- .../tests/test_aiopg_integration.py | 11 +++++++---- .../tests/mocks/lambda_function.py | 1 + .../tests/test_dbapi_integration.py | 3 +++ .../tests/test_aio_client_interceptor_hooks.py | 4 ++-- .../tests/test_client_interceptor_hooks.py | 4 ++-- .../tests/test_psycopg_integration.py | 6 +++--- .../tests/test_urllib_integration.py | 2 +- 8 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.pylintrc b/.pylintrc index 114dadef75..9f6c80faa9 100644 --- a/.pylintrc +++ b/.pylintrc @@ -492,4 +492,4 @@ min-public-methods=2 # Exceptions that will emit a warning when being caught. Defaults to # "Exception". -overgeneral-exceptions=Exception +overgeneral-exceptions=builtins.Exception diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py b/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py index fb76bd0f38..c497ae4564 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py @@ -17,6 +17,7 @@ from unittest.mock import MagicMock import aiopg +import psycopg2 import opentelemetry.instrumentation.aiopg from opentelemetry import trace as trace_api @@ -384,7 +385,9 @@ def test_span_failed(self): span.attributes[SpanAttributes.DB_STATEMENT], "Test query" ) self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) - self.assertEqual(span.status.description, "Exception: Test Exception") + self.assertEqual( + span.status.description, "ProgrammingError: Test Exception" + ) def test_executemany(self): db_integration = AiopgIntegration(self.tracer, "testcomponent") @@ -570,17 +573,17 @@ class MockCursor: # pylint: disable=unused-argument, no-self-use async def execute(self, query, params=None, throw_exception=False): if throw_exception: - raise Exception("Test Exception") + raise psycopg2.ProgrammingError("Test Exception") # pylint: disable=unused-argument, no-self-use async def executemany(self, query, params=None, throw_exception=False): if throw_exception: - raise Exception("Test Exception") + raise psycopg2.ProgrammingError("Test Exception") # pylint: disable=unused-argument, no-self-use async def callproc(self, query, params=None, throw_exception=False): if throw_exception: - raise Exception("Test Exception") + raise psycopg2.ProgrammingError("Test Exception") def close(self): pass diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py index a878d0f06a..539c896a0b 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py @@ -22,4 +22,5 @@ def rest_api_handler(event, context): def handler_exc(event, context): + # pylint: disable=broad-exception-raised raise Exception("500 internal server error") diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py index d0835e93e6..eb2d628a3a 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py @@ -419,11 +419,13 @@ def __init__(self) -> None: # pylint: disable=unused-argument, no-self-use def execute(self, query, params=None, throw_exception=False): if throw_exception: + # pylint: disable=broad-exception-raised raise Exception("Test Exception") # pylint: disable=unused-argument, no-self-use def executemany(self, query, params=None, throw_exception=False): if throw_exception: + # pylint: disable=broad-exception-raised raise Exception("Test Exception") self.query = query self.params = params @@ -431,4 +433,5 @@ def executemany(self, query, params=None, throw_exception=False): # pylint: disable=unused-argument, no-self-use def callproc(self, query, params=None, throw_exception=False): if throw_exception: + # pylint: disable=broad-exception-raised raise Exception("Test Exception") diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor_hooks.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor_hooks.py index fe906b26c1..9086d8b0f7 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor_hooks.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor_hooks.py @@ -47,11 +47,11 @@ def response_hook(span, response): def request_hook_with_exception(_span, _request): - raise Exception() + raise Exception() # pylint: disable=broad-exception-raised def response_hook_with_exception(_span, _response): - raise Exception() + raise Exception() # pylint: disable=broad-exception-raised @pytest.mark.asyncio diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor_hooks.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor_hooks.py index aeecffc71c..ac65c76c34 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor_hooks.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor_hooks.py @@ -73,11 +73,11 @@ def response_hook(span, response): def request_hook_with_exception(_span, _request): - raise Exception() + raise Exception() # pylint: disable=broad-exception-raised def response_hook_with_exception(_span, _response): - raise Exception() + raise Exception() # pylint: disable=broad-exception-raised class TestHooks(TestBase): diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py index 4fbcac6042..e2b3ed917a 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py @@ -53,17 +53,17 @@ def __init__(self, *args, **kwargs): # pylint: disable=unused-argument, no-self-use async def execute(self, query, params=None, throw_exception=False): if throw_exception: - raise Exception("Test Exception") + raise psycopg.Error("Test Exception") # pylint: disable=unused-argument, no-self-use async def executemany(self, query, params=None, throw_exception=False): if throw_exception: - raise Exception("Test Exception") + raise psycopg.Error("Test Exception") # pylint: disable=unused-argument, no-self-use async def callproc(self, query, params=None, throw_exception=False): if throw_exception: - raise Exception("Test Exception") + raise psycopg.Error("Test Exception") async def __aenter__(self, *args, **kwargs): return self diff --git a/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py b/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py index 36189e12c1..f73f0d1b97 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py +++ b/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py @@ -99,7 +99,7 @@ def timeout_exception_callback(*_, **__): @staticmethod def base_exception_callback(*_, **__): - raise Exception("test") + raise Exception("test") # pylint: disable=broad-exception-raised def assert_span(self, exporter=None, num_spans=1): if exporter is None: From 65b4f850a03135bff18a95e62465da881c25f0ec Mon Sep 17 00:00:00 2001 From: Tim Hutchinson Date: Fri, 24 May 2024 15:06:53 -0400 Subject: [PATCH 19/20] Preserve brackets around literal IPv6 hosts (#2552) --- CHANGELOG.md | 1 + .../src/opentelemetry/util/http/__init__.py | 6 +---- .../tests/test_remove_credentials.py | 27 +++++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 util/opentelemetry-util-http/tests/test_remove_credentials.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 038706f35c..3232e6fef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2420](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2420)) - `opentelemetry-instrumentation-asyncio` Check for __name__ attribute in the coroutine ([#2521](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2521)) +- `opentelemetry-util-http` Preserve brackets around literal IPv6 hosts ([#2552](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2552)) ## Version 1.24.0/0.45b0 (2024-03-28) diff --git a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py index 1f7ce98937..e8a2cf2034 100644 --- a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py +++ b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py @@ -166,11 +166,7 @@ def remove_url_credentials(url: str) -> str: parsed = urlparse(url) if all([parsed.scheme, parsed.netloc]): # checks for valid url parsed_url = urlparse(url) - netloc = ( - (":".join(((parsed_url.hostname or ""), str(parsed_url.port)))) - if parsed_url.port - else (parsed_url.hostname or "") - ) + _, _, netloc = parsed.netloc.rpartition("@") return urlunparse( ( parsed_url.scheme, diff --git a/util/opentelemetry-util-http/tests/test_remove_credentials.py b/util/opentelemetry-util-http/tests/test_remove_credentials.py new file mode 100644 index 0000000000..b6243145f5 --- /dev/null +++ b/util/opentelemetry-util-http/tests/test_remove_credentials.py @@ -0,0 +1,27 @@ +import unittest + +from opentelemetry.util.http import remove_url_credentials + + +class TestRemoveUrlCredentials(unittest.TestCase): + def test_remove_no_credentials(self): + url = "http://opentelemetry.io:8080/test/path?query=value" + cleaned_url = remove_url_credentials(url) + self.assertEqual(cleaned_url, url) + + def test_remove_credentials(self): + url = "http://someuser:somepass@opentelemetry.io:8080/test/path?query=value" + cleaned_url = remove_url_credentials(url) + self.assertEqual( + cleaned_url, "http://opentelemetry.io:8080/test/path?query=value" + ) + + def test_remove_credentials_ipv4_literal(self): + url = "http://someuser:somepass@127.0.0.1:8080/test/path?query=value" + cleaned_url = remove_url_credentials(url) + self.assertEqual(cleaned_url, "http://127.0.0.1:8080/test/path?query=value") + + def test_remove_credentials_ipv6_literal(self): + url = "http://someuser:somepass@[::1]:8080/test/path?query=value" + cleaned_url = remove_url_credentials(url) + self.assertEqual(cleaned_url, "http://[::1]:8080/test/path?query=value") From eb8e45695e88510a611d2cc04f14cdf56036968a Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 24 May 2024 22:13:37 +0200 Subject: [PATCH 20/20] elasticsearch: don't produce spans if native elasticsearch support is enabled (#2524) --- CHANGELOG.md | 3 ++ .../instrumentation/elasticsearch/__init__.py | 27 +++++++++++++- .../test-requirements-2.txt | 6 ++-- .../tests/test_elasticsearch.py | 35 +++++++++++++++++-- tox.ini | 2 +- 5 files changed, 66 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3232e6fef8..a75e1537cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,10 +54,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2474](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2474)) - `opentelemetry-instrumentation-elasticsearch` Improved support for version 8 ([#2420](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2420)) +- `opentelemetry-instrumentation-elasticsearch` Disabling instrumentation with native OTel support enabled + ([#2524](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2524)) - `opentelemetry-instrumentation-asyncio` Check for __name__ attribute in the coroutine ([#2521](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2521)) - `opentelemetry-util-http` Preserve brackets around literal IPv6 hosts ([#2552](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2552)) + ## Version 1.24.0/0.45b0 (2024-03-28) ### Added diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py index acf4596fb0..f8d7920e20 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py @@ -16,6 +16,15 @@ This library allows tracing HTTP elasticsearch made by the `elasticsearch `_ library. +.. warning:: + The elasticsearch package got native OpenTelemetry support since version + `8.13 `_. + To avoid duplicated tracing this instrumentation disables itself if it finds an elasticsearch client + that has OpenTelemetry support enabled. + + Please be aware that the two libraries may use a different semantic convention, see + `elasticsearch documentation `_. + Usage ----- @@ -54,7 +63,7 @@ def response_hook(span: Span, response: dict) for example: -.. code: python +.. code-block: python from opentelemetry.instrumentation.elasticsearch import ElasticsearchInstrumentor import elasticsearch @@ -81,6 +90,7 @@ def response_hook(span, response): """ import re +import warnings from logging import getLogger from os import environ from typing import Collection @@ -197,6 +207,16 @@ def _wrap_perform_request( ): # pylint: disable=R0912,R0914 def wrapper(wrapped, _, args, kwargs): + # if wrapped elasticsearch has native OTel instrumentation just call the wrapped function + otel_span = kwargs.get("otel_span") + if otel_span and otel_span.otel_span: + warnings.warn( + "Instrumentation disabled, relying on elasticsearch native OTel support, see " + "https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/elasticsearch/elasticsearch.html", + Warning, + ) + return wrapped(*args, **kwargs) + method = url = None try: method, url, *_ = args @@ -249,6 +269,11 @@ def normalize_kwargs(k, v): v = str(v) elif isinstance(v, elastic_transport.HttpHeaders): v = dict(v) + elif isinstance( + v, elastic_transport.OpenTelemetrySpan + ): + # the transport Span is always a dummy one + v = None return (k, v) hook_kwargs = dict( diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt index 23d87f93dd..6b0d677ec7 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt @@ -1,9 +1,9 @@ asgiref==3.7.2 attrs==23.2.0 Deprecated==1.2.14 -elasticsearch==8.12.1 -elasticsearch-dsl==8.12.0 -elastic-transport==8.12.0 +elasticsearch==8.13.1 +elasticsearch-dsl==8.13.1 +elastic-transport==8.13.0 importlib-metadata==6.11.0 iniconfig==2.0.0 packaging==23.2 diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py index b0ee170329..b7e24d87c9 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py @@ -23,6 +23,7 @@ import elasticsearch.exceptions from elasticsearch import Elasticsearch from elasticsearch_dsl import Search +from pytest import mark import opentelemetry.instrumentation.elasticsearch from opentelemetry import trace @@ -36,7 +37,7 @@ from . import sanitization_queries # pylint: disable=no-name-in-module -major_version = elasticsearch.VERSION[0] +major_version, minor_version = elasticsearch.VERSION[:2] if major_version == 8: from . import helpers_es8 as helpers # pylint: disable=no-name-in-module @@ -70,6 +71,9 @@ def get_elasticsearch_client(*args, **kwargs): @mock.patch(helpers.perform_request_mock_path) +@mock.patch.dict( + os.environ, {"OTEL_PYTHON_INSTRUMENTATION_ELASTICSEARCH_ENABLED": "false"} +) class TestElasticsearchIntegration(TestBase): search_attributes = { SpanAttributes.DB_SYSTEM: "elasticsearch", @@ -110,7 +114,6 @@ def test_instrumentor(self, request_mock): span = spans_list[0] # Check version and name in span's instrumentation info - # self.assertEqualSpanInstrumentationInfo(span, opentelemetry.instrumentation.elasticsearch) self.assertEqualSpanInstrumentationInfo( span, opentelemetry.instrumentation.elasticsearch ) @@ -475,6 +478,7 @@ def request_hook(span, method, url, kwargs): "headers": { "accept": "application/vnd.elasticsearch+json; compatible-with=8" }, + "otel_span": None, } elif major_version == 7: expected_kwargs = { @@ -607,3 +611,30 @@ def test_bulk(self, request_mock): self.assertEqualSpanInstrumentationInfo( span, opentelemetry.instrumentation.elasticsearch ) + + @mark.skipif( + (major_version, minor_version) < (8, 13), + reason="Native OTel since elasticsearch 8.13", + ) + @mock.patch.dict( + os.environ, + {"OTEL_PYTHON_INSTRUMENTATION_ELASTICSEARCH_ENABLED": "true"}, + ) + def test_instrumentation_is_disabled_if_native_support_enabled( + self, request_mock + ): + request_mock.return_value = helpers.mock_response("{}") + + es = get_elasticsearch_client(hosts=["http://localhost:9200"]) + es.index( + index="sw", + id=1, + **normalize_arguments(body={"name": "adam"}, doc_type="_doc"), + ) + + spans_list = self.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + # Check that name in span's instrumentation info is not from this instrumentation + self.assertEqual(span.instrumentation_info.name, "elasticsearch-api") diff --git a/tox.ini b/tox.ini index d6fb9eee10..a014306640 100644 --- a/tox.ini +++ b/tox.ini @@ -92,7 +92,7 @@ envlist = ; below mean these dependencies are being used: ; 0: elasticsearch-dsl==6.4.0 elasticsearch==6.8.2 ; 1: elasticsearch-dsl==7.4.1 elasticsearch==7.17.9 - ; 2: elasticsearch-dsl>=8.0,<8.13 elasticsearch>=8.0,<8.13 + ; 2: elasticsearch-dsl==8.13.1 elasticsearch==8.13.1 py3{8,9,10,11}-test-instrumentation-elasticsearch-{0,1,2} pypy3-test-instrumentation-elasticsearch-{0,1,2} lint-instrumentation-elasticsearch