From 1d3dea0475af94399f6b251c67712272831d40ae Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Sat, 4 May 2024 16:47:19 -0400 Subject: [PATCH 01/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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, {})