diff --git a/.pylintrc b/.pylintrc index 9f6c80faa9..39ea7b5d35 100644 --- a/.pylintrc +++ b/.pylintrc @@ -45,6 +45,9 @@ suggestion-mode=yes # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no +# Run python dependant checks considering the baseline version +py-version=3.8 + [MESSAGES CONTROL] diff --git a/CHANGELOG.md b/CHANGELOG.md index e17542b104..f0fd2243dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- `opentelemetry-instrumentation-aws-lambda` Bugfix: AWS Lambda event source key incorrect for SNS in instrumentation library. + ([#2612](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2612)) +- `opentelemetry-instrumentation-system-metrics` Permit to use psutil 6.0+. + ([#2630](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2630)) + +### Added + +- `opentelemetry-instrumentation-pyramid` Record exceptions raised when serving a request + ([#2622](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2622)) +- `opentelemetry-sdk-extension-aws` Add AwsXrayLambdaPropagator + ([#2573](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2573)) +- `opentelemetry-instrumentation-confluent-kafka` Add support for version 2.4.0 of confluent_kafka + ([#2616](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2616)) +- `opentelemetry-instrumentation-confluent-kafka` Add support for produce purge + ([#2638](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2638)) + ### Breaking changes - `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-fastapi`, `opentelemetry-instrumentation-starlette` Use `tracer` and `meter` of originating components instead of one from `asgi` middleware @@ -18,6 +34,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2538](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2538)) - Add Python 3.12 support ([#2572](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2572)) +- `opentelemetry-instrumentation-aiohttp-server`, `opentelemetry-instrumentation-httpx` Ensure consistently use of suppress_instrumentation utils + ([#2590](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2590)) +- Reference symbols from generated semantic conventions + ([#2611](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2611)) ## Version 1.25.0/0.46b0 (2024-05-31) @@ -28,13 +48,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2300](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2300)) - Rename AwsLambdaInstrumentor span attributes `faas.id` to `cloud.resource_id`, `faas.execution` to `faas.invocation_id` ([#2372](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2372)) -- Drop support for instrumenting elasticsearch client < 6` +- Drop support for instrumenting elasticsearch client < 6 ([#2422](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2422)) - `opentelemetry-instrumentation-wsgi` Add `http.method` to `span.name` ([#2425](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2425)) - `opentelemetry-instrumentation-flask` Add `http.method` to `span.name` ([#2454](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2454)) -- ASGI, FastAPI, Starlette: provide both send and receive hooks with `scope` and `message` for internal spans ([#2546](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2546)) +- Record repeated HTTP headers in lists, rather than a comma separate strings for ASGI based web frameworks + ([#2361](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2361)) +- ASGI, FastAPI, Starlette: provide both send and receive hooks with `scope` and `message` for internal spans +- ([#2546](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2546)) ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2cd72f12d8..f5a4913440 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,18 +55,18 @@ some aspects of development, including testing against multiple Python versions. To install `tox`, run: ```sh -pip install tox +$ pip install tox ``` You can run `tox` with the following arguments: -- `tox` to run all existing tox commands, including unit tests for all packages +* `tox` to run all existing tox commands, including unit tests for all packages under multiple Python versions -- `tox -e docs` to regenerate the API docs -- `tox -e py312-test-instrumentation-aiopg` to e.g. run the aiopg instrumentation unit tests under a specific +* `tox -e docs` to regenerate the API docs +* `tox -e py312-test-instrumentation-aiopg` to e.g. run the aiopg instrumentation unit tests under a specific Python version -- `tox -e spellcheck` to run a spellcheck on all the code -- `tox -e lint-some-package` to run lint checks on `some-package` +* `tox -e spellcheck` to run a spellcheck on all the code +* `tox -e lint-some-package` to run lint checks on `some-package` `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: @@ -84,6 +84,7 @@ You can also configure it to run lint tools automatically before committing with ```console $ pre-commit install +``` See [`tox.ini`](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/tox.ini) @@ -128,13 +129,14 @@ pull requests (PRs). To create a new PR, fork the project in GitHub and clone the upstream repo: ```sh -git clone https://github.com/open-telemetry/opentelemetry-python-contrib.git +$ git clone https://github.com/open-telemetry/opentelemetry-python-contrib.git +$ cd opentelemetry-python-contrib ``` Add your fork as an origin: ```sh -git remote add fork https://github.com/YOUR_GITHUB_USERNAME/opentelemetry-python-contrib.git +$ git remote add fork https://github.com/YOUR_GITHUB_USERNAME/opentelemetry-python-contrib.git ``` Run tests: @@ -148,10 +150,10 @@ $ tox # execute in the root of the repository Check out a new branch, make modifications and push the branch to your fork: ```sh -git checkout -b feature +$ git checkout -b feature # edit files -git commit -git push fork feature +$ git commit +$ git push fork feature ``` Open a pull request against the main `opentelemetry-python-contrib` repo. @@ -160,6 +162,7 @@ Open a pull request against the main `opentelemetry-python-contrib` repo. * If the PR is not ready for review, please put `[WIP]` in the title, tag it as `work-in-progress`, or mark it as [`draft`](https://github.blog/2019-02-14-introducing-draft-pull-requests/). +* Make sure tests and lint are passing locally before requesting a review. * Make sure CLA is signed and CI is clear. ### How to Get PRs Reviewed @@ -215,13 +218,26 @@ For a deeper discussion, see: https://github.com/open-telemetry/opentelemetry-sp 2. Make sure you have `tox` installed. `pip install tox`. 3. Run `tox` without any arguments to run tests for all the packages. Read more about [tox](https://tox.readthedocs.io/en/latest/). +Some tests can be slow due to pre-steps that do dependencies installs. To help with that, you can run tox a first time, and after that run the tests using previous installed dependencies in toxdir as following: + +1. First time run (e.g., opentelemetry-instrumentation-aiopg) +```console +tox -e py312-test-instrumentation-aiopg +``` +2. Run tests again without pre-steps: +```console +.tox/py312-test-instrumentation-aiopg/bin/pytest instrumentation/opentelemetry-instrumentation-aiopg +``` + ### Testing against a different Core repo branch/commit Some of the tox targets install packages from the [OpenTelemetry Python Core Repository](https://github.com/open-telemetry/opentelemetry-python) via pip. The version of the packages installed defaults to the main branch in that repository when tox is run locally. It is possible to install packages tagged with a specific git commit hash by setting an environment variable before running tox as per the following example: +```sh CORE_REPO_SHA=c49ad57bfe35cfc69bfa863d74058ca9bec55fc3 tox +``` -The continuation integration overrides that environment variable with as per the configuration [here](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/2518a4ac07cb62ad6587dd8f6cbb5f8663a7e179/.github/workflows/test.yml#L9). +The continuous integration overrides that environment variable with as per the configuration [here](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/.github/workflows/test.yml#L9). ## Style Guide @@ -259,3 +275,24 @@ Below is a checklist of things to be mindful of when implementing a new instrume ## Expectations from contributors OpenTelemetry is an open source community, and as such, greatly encourages contributions from anyone interested in the project. With that being said, there is a certain level of expectation from contributors even after a pull request is merged, specifically pertaining to instrumentations. The OpenTelemetry Python community expects contributors to maintain a level of support and interest in the instrumentations they contribute. This is to ensure that the instrumentation does not become stale and still functions the way the original contributor intended. Some instrumentations also pertain to libraries that the current members of the community are not so familiar with, so it is necessary to rely on the expertise of the original contributing parties. + +## Updating supported Python versions + +### Bumping the Python baseline + +When updating the minimum supported Python version remember to: + +- Remove the version in `pyproject.toml` trove classifiers +- Remove the version from `tox.ini` +- Search for `sys.version_info` usage and remove code for unsupported versions +- Bump `py-version` in `.pylintrc` for Python version dependent checks + +### Adding support for a new Python release + +When adding support for a new Python release remember to: + +- Add the version in `tox.ini` +- Add the version in `pyproject.toml` trove classifiers +- Update github workflows accordingly; lint and benchmarks use the latest supported version +- Update `.pre-commit-config.yaml` +- Update tox examples in the documentation diff --git a/README.md b/README.md index 133acf81f6..6c019239ae 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,6 @@ The Python auto-instrumentation libraries for [OpenTelemetry](https://openteleme * [Releasing](#releasing) * [Releasing a package as `1.0` stable](#releasing-a-package-as-10-stable) * [Contributing](#contributing) -* [Running Tests Locally](#running-tests-locally) * [Thanks to all the people who already contributed](#thanks-to-all-the-people-who-already-contributed) ## Installation @@ -143,14 +142,6 @@ Emeritus Maintainers: *Find more about the maintainer role in [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer).* -## Running Tests Locally - -1. Go to your Contrib repo directory. `cd ~/git/opentelemetry-python-contrib`. -2. Create a virtual env in your Contrib repo directory. `python3 -m venv my_test_venv`. -3. Activate your virtual env. `source my_test_venv/bin/activate`. -4. Make sure you have `tox` installed. `pip install tox`. -5. Run tests for a package. (e.g. `tox -e test-instrumentation-flask`.) - ### Thanks to all the people who already contributed diff --git a/dev-requirements.txt b/dev-requirements.txt index 60db203e2d..3289650ac8 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -14,7 +14,7 @@ bleach==4.1.0 # transient dependency for readme-renderer protobuf~=3.13 markupsafe>=2.0.1 codespell==2.1.0 -requests==2.31.0 +requests==2.32.3 ruamel.yaml==0.17.21 flaky==3.7.0 pre-commit==3.7.0; python_version >= '3.9' diff --git a/docs-requirements.txt b/docs-requirements.txt index 72f4472902..34ce74ca7a 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -27,7 +27,7 @@ botocore~=1.0 boto3~=1.0 cassandra-driver~=3.25 celery>=4.0 -confluent-kafka>= 1.8.2,<= 2.3.0 +confluent-kafka>= 1.8.2,<= 2.4.0 elasticsearch>=6.0,<9.0 flask~=2.0 falcon~=2.0 @@ -38,8 +38,8 @@ mysqlclient~=2.1.1 psutil>=5 psycopg~=3.1.17 pika>=0.12.0 -pymongo~=3.1 -PyMySQL~=0.9.3 +pymongo~=4.6.3 +PyMySQL~=1.1.1 pyramid>=1.7 redis>=2.6 remoulade>=0.50 diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt b/exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt index ffd5916ae7..b41245cd1f 100644 --- a/exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt @@ -16,7 +16,7 @@ python-snappy==0.7.1 requests==2.32.3 tomli==2.0.1 typing_extensions==4.10.0 -urllib3==2.2.1 +urllib3==2.2.2 wrapt==1.16.0 zipp==3.17.0 -e exporter/opentelemetry-exporter-prometheus-remote-write diff --git a/instrumentation/README.md b/instrumentation/README.md index 5dfed03e9a..682334db1a 100644 --- a/instrumentation/README.md +++ b/instrumentation/README.md @@ -14,7 +14,7 @@ | [opentelemetry-instrumentation-botocore](./opentelemetry-instrumentation-botocore) | botocore ~= 1.0 | No | experimental | [opentelemetry-instrumentation-cassandra](./opentelemetry-instrumentation-cassandra) | cassandra-driver ~= 3.25,scylla-driver ~= 3.25 | No | experimental | [opentelemetry-instrumentation-celery](./opentelemetry-instrumentation-celery) | celery >= 4.0, < 6.0 | No | experimental -| [opentelemetry-instrumentation-confluent-kafka](./opentelemetry-instrumentation-confluent-kafka) | confluent-kafka >= 1.8.2, <= 2.3.0 | No | experimental +| [opentelemetry-instrumentation-confluent-kafka](./opentelemetry-instrumentation-confluent-kafka) | confluent-kafka >= 1.8.2, <= 2.4.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 >= 6.0 | No | experimental diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-aiohttp-client/test-requirements.txt index 0a9c451d33..f597361598 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/test-requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.9.3 +aiohttp==3.9.4 aiosignal==1.3.1 asgiref==3.7.2 async-timeout==4.0.3 @@ -25,7 +25,7 @@ pytest-benchmark==4.0.0 requests==2.32.3 tomli==2.0.1 typing_extensions==4.10.0 -urllib3==2.2.1 +urllib3==2.2.2 Werkzeug==3.0.3 wrapt==1.16.0 yarl==1.9.4 diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py index 2fa97f40b0..b6fe1eb57a 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -14,7 +14,6 @@ import asyncio import contextlib -import sys import typing import unittest import urllib.parse @@ -117,10 +116,6 @@ def test_status_codes(self): ) url = f"http://{host}:{port}/test-path?query=param#foobar" - # if python version is < 3.8, then the url will be - if sys.version_info[1] < 8: - url = f"http://{host}:{port}/test-path#foobar" - self.assert_spans( [ ( diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py index c1ab960818..2e519ac1c5 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py @@ -19,12 +19,14 @@ from aiohttp import web from multidict import CIMultiDictProxy -from opentelemetry import context, metrics, trace -from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY +from opentelemetry import metrics, trace from opentelemetry.instrumentation.aiohttp_server.package import _instruments from opentelemetry.instrumentation.aiohttp_server.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.utils import http_status_to_status_code +from opentelemetry.instrumentation.utils import ( + http_status_to_status_code, + is_http_instrumentation_enabled, +) from opentelemetry.propagate import extract from opentelemetry.propagators.textmap import Getter from opentelemetry.semconv.metrics import MetricInstruments @@ -191,10 +193,8 @@ def keys(self, carrier: Dict) -> List: @web.middleware async def middleware(request, handler): """Middleware for aiohttp implementing tracing logic""" - if ( - context.get_value("suppress_instrumentation") - or context.get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY) - or _excluded_urls.url_disabled(request.url.path) + if not is_http_instrumentation_enabled() or _excluded_urls.url_disabled( + request.url.path ): return await handler(request) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt index c62da59804..fe7582a2bb 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.9.3 +aiohttp==3.9.4 aiosignal==1.3.1 asgiref==3.7.2 async-timeout==4.0.3 diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py index b5e8ec468f..e9dfb11389 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py @@ -23,6 +23,7 @@ from opentelemetry.instrumentation.aiohttp_server import ( AioHttpServerInstrumentor, ) +from opentelemetry.instrumentation.utils import suppress_http_instrumentation from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.globals_test import reset_trace_globals from opentelemetry.test.test_base import TestBase @@ -64,16 +65,25 @@ async def default_handler(request, status=200): return aiohttp.web.Response(status=status) +@pytest.fixture(name="suppress") +def fixture_suppress(): + return False + + @pytest_asyncio.fixture(name="server_fixture") -async def fixture_server_fixture(tracer, aiohttp_server): +async def fixture_server_fixture(tracer, aiohttp_server, suppress): _, memory_exporter = tracer AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() app.add_routes([aiohttp.web.get("/test-path", default_handler)]) + if suppress: + with suppress_http_instrumentation(): + server = await aiohttp_server(app) + else: + server = await aiohttp_server(app) - server = await aiohttp_server(app) yield server, app memory_exporter.clear() @@ -128,3 +138,18 @@ async def test_status_code_instrumentation( f"http://{server.host}:{server.port}{url}" == span.attributes[SpanAttributes.HTTP_URL] ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("suppress", [True]) +async def test_suppress_instrumentation( + tracer, server_fixture, aiohttp_client +): + _, memory_exporter = tracer + server, _ = server_fixture + assert len(memory_exporter.get_finished_spans()) == 0 + + client = await aiohttp_client(server) + await client.get("/test-path") + + assert len(memory_exporter.get_finished_spans()) == 0 diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index e416e8dec2..f24160fcb6 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -129,10 +129,10 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a -single item list containing all the header values. +list containing the header values. For example: -``http.request.header.custom_request_header = [","]`` +``http.request.header.custom_request_header = ["", ""]`` Response headers **************** @@ -163,10 +163,10 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a -single item list containing all the header values. +list containing the header values. For example: -``http.response.header.custom_response_header = [","]`` +``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** @@ -193,9 +193,10 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A import typing import urllib +from collections import defaultdict from functools import wraps from timeit import default_timer -from typing import Any, Awaitable, Callable, Tuple +from typing import Any, Awaitable, Callable, DefaultDict, Tuple from asgiref.compatibility import guarantee_single_callable @@ -340,24 +341,19 @@ def collect_custom_headers_attributes( sanitize: SanitizeValue, header_regexes: list[str], normalize_names: Callable[[str], str], -) -> dict[str, str]: +) -> dict[str, list[str]]: """ Returns custom HTTP request or response headers to be added into SERVER span as span attributes. Refer specifications: - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers """ - # Decode headers before processing. - headers: dict[str, str] = {} + headers: DefaultDict[str, list[str]] = defaultdict(list) raw_headers = scope_or_response_message.get("headers") if raw_headers: - for _key, _value in raw_headers: - key = _key.decode().lower() - value = _value.decode() - if key in headers: - headers[key] += f",{value}" - else: - headers[key] = value + for key, value in raw_headers: + # Decode headers before processing. + headers[key.decode()].append(value.decode()) return sanitize.sanitize_header_values( headers, diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py index 7a9c91a3e7..5394d62ff0 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py @@ -152,7 +152,8 @@ def test_http_repeat_request_headers_in_span_attributes(self): span_list = self.exporter.get_finished_spans() expected = { "http.request.header.custom_test_header_1": ( - "test-header-value-1,test-header-value-2", + "test-header-value-1", + "test-header-value-2", ), } span = next(span for span in span_list if span.kind == SpanKind.SERVER) @@ -225,7 +226,8 @@ def test_http_repeat_response_headers_in_span_attributes(self): span_list = self.exporter.get_finished_spans() expected = { "http.response.header.custom_test_header_1": ( - "test-header-value-1,test-header-value-2", + "test-header-value-1", + "test-header-value-2", ), } span = next(span for span in span_list if span.kind == SpanKind.SERVER) diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py index 7614ba9813..4acf4dea90 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py @@ -306,9 +306,11 @@ def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches disable_aws_context_propagation, ) - span_kind = None try: - if lambda_event["Records"][0]["eventSource"] in { + event_source = lambda_event["Records"][0].get( + "eventSource" + ) or lambda_event["Records"][0].get("EventSource") + if event_source in { "aws:sqs", "aws:s3", "aws:sns", diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py index f10953c754..ecce9ea12c 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py @@ -349,12 +349,43 @@ def test_lambda_handles_multiple_consumers(self): mock_execute_lambda({"Records": [{"eventSource": "aws:sqs"}]}) mock_execute_lambda({"Records": [{"eventSource": "aws:s3"}]}) - mock_execute_lambda({"Records": [{"eventSource": "aws:sns"}]}) + mock_execute_lambda({"Records": [{"EventSource": "aws:sns"}]}) mock_execute_lambda({"Records": [{"eventSource": "aws:dynamodb"}]}) spans = self.memory_exporter.get_finished_spans() assert spans + assert len(spans) == 4 + + for span in spans: + assert span.kind == SpanKind.CONSUMER + + test_env_patch.stop() + + def test_lambda_handles_invalid_event_source(self): + test_env_patch = mock.patch.dict( + "os.environ", + { + **os.environ, + # NOT Active Tracing + _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, + # NOT using the X-Ray Propagator + OTEL_PROPAGATORS: "tracecontext", + }, + ) + test_env_patch.start() + + AwsLambdaInstrumentor().instrument() + + mock_execute_lambda({"Records": [{"eventSource": "invalid_source"}]}) + + spans = self.memory_exporter.get_finished_spans() + + assert spans + assert len(spans) == 1 + assert ( + spans[0].kind == SpanKind.SERVER + ) # Default to SERVER for unknown sources test_env_patch.stop() diff --git a/instrumentation/opentelemetry-instrumentation-boto/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-boto/test-requirements.txt index 8d8aad2f0e..54c8bb0558 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-boto/test-requirements.txt @@ -5,7 +5,7 @@ botocore==1.34.44 certifi==2024.2.2 cffi==1.16.0 charset-normalizer==3.3.2 -cryptography==42.0.3 +cryptography==42.0.5 Deprecated==1.2.14 docker==7.0.0 idna==3.7 @@ -30,7 +30,7 @@ s3transfer==0.10.0 six==1.16.0 tomli==2.0.1 typing_extensions==4.9.0 -urllib3==1.26.18 +urllib3==1.26.19 Werkzeug==2.3.8 wrapt==1.16.0 xmltodict==0.13.0 diff --git a/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt index 8ddfd9983f..54fcf790d7 100644 --- a/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt @@ -15,7 +15,7 @@ s3transfer==0.10.0 six==1.16.0 tomli==2.0.1 typing_extensions==4.9.0 -urllib3==1.26.18 +urllib3==1.26.19 wrapt==1.16.0 zipp==3.17.0 -e opentelemetry-instrumentation diff --git a/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt index f52060f22e..f9fe9abe8a 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt @@ -13,8 +13,8 @@ importlib-metadata==6.11.0 iniconfig==2.0.0 Jinja2==3.1.4 jmespath==1.0.1 -MarkupSafe==2.0.1 -moto==3.1.19 +MarkupSafe==2.1.5 +moto==5.0.9 packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 @@ -30,8 +30,8 @@ s3transfer==0.7.0 six==1.16.0 tomli==2.0.1 typing_extensions==4.9.0 -urllib3==1.26.18 -Werkzeug==2.1.2 +urllib3==1.26.19 +Werkzeug==3.0.3 wrapt==1.16.0 xmltodict==0.13.0 zipp==3.17.0 diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_dynamodb.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_dynamodb.py index 12ebe8f2b7..2240baff3a 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_dynamodb.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_dynamodb.py @@ -16,7 +16,7 @@ from unittest import mock import botocore.session -from moto import mock_dynamodb2 # pylint: disable=import-error +from moto import mock_aws # pylint: disable=import-error from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.instrumentation.botocore.extensions.dynamodb import ( @@ -184,7 +184,7 @@ def assert_extension_item_col_metrics(self, operation: str): ) self.assert_item_col_metrics(span) - @mock_dynamodb2 + @mock_aws def test_batch_get_item(self): table_name1 = "test_table1" table_name2 = "test_table2" @@ -203,7 +203,7 @@ def test_batch_get_item(self): self.assert_table_names(span, table_name1, table_name2) self.assert_consumed_capacity(span, table_name1, table_name2) - @mock_dynamodb2 + @mock_aws def test_batch_write_item(self): table_name1 = "test_table1" table_name2 = "test_table2" @@ -224,7 +224,7 @@ def test_batch_write_item(self): self.assert_consumed_capacity(span, table_name1, table_name2) self.assert_item_col_metrics(span) - @mock_dynamodb2 + @mock_aws def test_create_table(self): local_sec_idx = { "IndexName": "local_sec_idx", @@ -268,7 +268,7 @@ def test_create_table(self): ) self.assert_provisioned_read_cap(span, 42) - @mock_dynamodb2 + @mock_aws def test_delete_item(self): self._create_prepared_table() @@ -297,7 +297,7 @@ def test_delete_item_consumed_capacity(self): def test_delete_item_item_collection_metrics(self): self.assert_extension_item_col_metrics("DeleteItem") - @mock_dynamodb2 + @mock_aws def test_delete_table(self): self._create_prepared_table() @@ -306,7 +306,7 @@ def test_delete_table(self): span = self.assert_span("DeleteTable") self.assert_table_names(span, self.default_table_name) - @mock_dynamodb2 + @mock_aws def test_describe_table(self): self._create_prepared_table() @@ -315,15 +315,31 @@ def test_describe_table(self): span = self.assert_span("DescribeTable") self.assert_table_names(span, self.default_table_name) - @mock_dynamodb2 - def test_get_item(self): + @mock_aws + def test_get_item_expression(self): + self._create_prepared_table() + + self.client.get_item( + TableName=self.default_table_name, + Key={"id": {"S": "1"}}, + ConsistentRead=True, + 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_consumed_capacity(span, self.default_table_name) + + @mock_aws + def test_get_item_non_expression(self): self._create_prepared_table() self.client.get_item( TableName=self.default_table_name, Key={"id": {"S": "1"}}, ConsistentRead=True, - AttributesToGet=["id"], ProjectionExpression="PE", ReturnConsumedCapacity="TOTAL", ) @@ -334,7 +350,7 @@ def test_get_item(self): self.assert_projection(span, "PE") self.assert_consumed_capacity(span, self.default_table_name) - @mock_dynamodb2 + @mock_aws def test_list_tables(self): self._create_table(TableName="my_table") self._create_prepared_table() @@ -351,7 +367,7 @@ def test_list_tables(self): ) self.assertEqual(5, span.attributes[SpanAttributes.AWS_DYNAMODB_LIMIT]) - @mock_dynamodb2 + @mock_aws def test_put_item(self): table = "test_table" self._create_prepared_table(TableName=table) @@ -372,7 +388,7 @@ def test_put_item(self): def test_put_item_item_collection_metrics(self): self.assert_extension_item_col_metrics("PutItem") - @mock_dynamodb2 + @mock_aws def test_query(self): self._create_prepared_table() @@ -407,7 +423,7 @@ def test_query(self): self.assert_select(span, "ALL_ATTRIBUTES") self.assert_consumed_capacity(span, self.default_table_name) - @mock_dynamodb2 + @mock_aws def test_scan(self): self._create_prepared_table() @@ -444,7 +460,7 @@ def test_scan(self): self.assert_select(span, "ALL_ATTRIBUTES") self.assert_consumed_capacity(span, self.default_table_name) - @mock_dynamodb2 + @mock_aws def test_update_item(self): self._create_prepared_table() @@ -465,7 +481,7 @@ def test_update_item(self): def test_update_item_item_collection_metrics(self): self.assert_extension_item_col_metrics("UpdateItem") - @mock_dynamodb2 + @mock_aws def test_update_table(self): self._create_prepared_table() diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py index bb6d283399..62357a3336 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py @@ -12,19 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. import json -from unittest.mock import Mock, patch +from unittest.mock import ANY, Mock, patch import botocore.session from botocore.exceptions import ParamValidationError -from moto import ( # pylint: disable=import-error - mock_ec2, - mock_kinesis, - mock_kms, - mock_s3, - mock_sqs, - mock_sts, - mock_xray, -) +from moto import mock_aws # pylint: disable=import-error from opentelemetry import trace as trace_api from opentelemetry.instrumentation.botocore import BotocoreInstrumentor @@ -39,7 +31,7 @@ from opentelemetry.test.test_base import TestBase from opentelemetry.trace.span import format_span_id, format_trace_id -_REQUEST_ID_REGEX_MATCH = r"[A-Z0-9]{52}" +_REQUEST_ID_REGEX_MATCH = r"[A-Za-z0-9]{52}" # pylint:disable=too-many-public-methods @@ -102,7 +94,7 @@ def assert_span( self.assertEqual(f"{service}.{operation}", span.name) return span - @mock_ec2 + @mock_aws def test_traced_client(self): ec2 = self._make_client("ec2") @@ -111,7 +103,7 @@ def test_traced_client(self): request_id = "fdcdcab1-ae5c-489e-9c33-4637c5dda355" self.assert_span("EC2", "DescribeInstances", request_id=request_id) - @mock_ec2 + @mock_aws def test_not_recording(self): mock_tracer = Mock() mock_span = Mock() @@ -126,7 +118,7 @@ def test_not_recording(self): self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) - @mock_s3 + @mock_aws def test_exception(self): s3 = self._make_client("s3") @@ -149,14 +141,14 @@ def test_exception(self): self.assertIn(SpanAttributes.EXCEPTION_TYPE, event.attributes) self.assertIn(SpanAttributes.EXCEPTION_MESSAGE, event.attributes) - @mock_s3 + @mock_aws def test_s3_client(self): s3 = self._make_client("s3") s3.list_buckets() self.assert_span("S3", "ListBuckets") - @mock_s3 + @mock_aws def test_s3_put(self): s3 = self._make_client("s3") @@ -174,7 +166,7 @@ def test_s3_put(self): s3.get_object(Bucket="mybucket", Key="foo") self.assert_span("S3", "GetObject", request_id=_REQUEST_ID_REGEX_MATCH) - @mock_sqs + @mock_aws def test_sqs_client(self): sqs = self._make_client("sqs") @@ -184,7 +176,7 @@ def test_sqs_client(self): "SQS", "ListQueues", request_id=_REQUEST_ID_REGEX_MATCH ) - @mock_sqs + @mock_aws def test_sqs_send_message(self): sqs = self._make_client("sqs") test_queue_name = "test_queue_name" @@ -205,14 +197,14 @@ def test_sqs_send_message(self): attributes={"aws.queue_url": queue_url}, ) - @mock_kinesis + @mock_aws def test_kinesis_client(self): kinesis = self._make_client("kinesis") kinesis.list_streams() self.assert_span("Kinesis", "ListStreams") - @mock_kinesis + @mock_aws def test_unpatch(self): kinesis = self._make_client("kinesis") @@ -221,7 +213,7 @@ def test_unpatch(self): kinesis.list_streams() self.assertEqual(0, len(self.memory_exporter.get_finished_spans())) - @mock_ec2 + @mock_aws def test_uninstrument_does_not_inject_headers(self): headers = {} @@ -240,7 +232,7 @@ def intercept_headers(**kwargs): self.assertNotIn(TRACE_HEADER_KEY, headers) - @mock_sqs + @mock_aws def test_double_patch(self): sqs = self._make_client("sqs") @@ -252,19 +244,19 @@ def test_double_patch(self): "SQS", "ListQueues", request_id=_REQUEST_ID_REGEX_MATCH ) - @mock_kms + @mock_aws def test_kms_client(self): kms = self._make_client("kms") kms.list_keys(Limit=21) span = self.assert_only_span() + expected = self._default_span_attributes("KMS", "ListKeys") + expected["aws.request_id"] = ANY # check for exact attribute set to make sure not to leak any kms secrets - self.assertEqual( - self._default_span_attributes("KMS", "ListKeys"), span.attributes - ) + self.assertEqual(expected, dict(span.attributes)) - @mock_sts + @mock_aws def test_sts_client(self): sts = self._make_client("sts") @@ -272,11 +264,11 @@ def test_sts_client(self): span = self.assert_only_span() expected = self._default_span_attributes("STS", "GetCallerIdentity") - expected["aws.request_id"] = "c6104cbe-af31-11e0-8154-cbc7ccf896c7" + expected["aws.request_id"] = ANY # check for exact attribute set to make sure not to leak any sts secrets - self.assertEqual(expected, span.attributes) + self.assertEqual(expected, dict(span.attributes)) - @mock_ec2 + @mock_aws def test_propagator_injects_into_request(self): headers = {} previous_propagator = get_global_textmap() @@ -316,7 +308,7 @@ def check_headers(**kwargs): finally: set_global_textmap(previous_propagator) - @mock_ec2 + @mock_aws def test_override_xray_propagator_injects_into_request(self): headers = {} @@ -335,7 +327,7 @@ def check_headers(**kwargs): self.assertNotIn(MockTextMapPropagator.TRACE_ID_KEY, headers) self.assertNotIn(MockTextMapPropagator.SPAN_ID_KEY, headers) - @mock_xray + @mock_aws def test_suppress_instrumentation_xray_client(self): xray_client = self._make_client("xray") with suppress_instrumentation(): @@ -343,7 +335,7 @@ def test_suppress_instrumentation_xray_client(self): xray_client.put_trace_segments(TraceSegmentDocuments=["str2"]) self.assertEqual(0, len(self.get_finished_spans())) - @mock_xray + @mock_aws def test_suppress_http_instrumentation_xray_client(self): xray_client = self._make_client("xray") with suppress_http_instrumentation(): @@ -351,7 +343,7 @@ def test_suppress_http_instrumentation_xray_client(self): xray_client.put_trace_segments(TraceSegmentDocuments=["str2"]) self.assertEqual(2, len(self.get_finished_spans())) - @mock_s3 + @mock_aws def test_request_hook(self): request_hook_service_attribute_name = "request_hook.service_name" request_hook_operation_attribute_name = "request_hook.operation_name" @@ -386,7 +378,7 @@ def request_hook(span, service_name, operation_name, api_params): }, ) - @mock_s3 + @mock_aws def test_response_hook(self): response_hook_service_attribute_name = "request_hook.service_name" response_hook_operation_attribute_name = "response_hook.operation_name" diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py index 7388323100..098edfc896 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py @@ -19,7 +19,7 @@ from unittest import mock import botocore.session -from moto import mock_iam, mock_lambda # pylint: disable=import-error +from moto import mock_aws # pylint: disable=import-error from pytest import mark from opentelemetry.instrumentation.botocore import BotocoreInstrumentor @@ -96,12 +96,12 @@ def _create_extension(operation: str) -> _LambdaExtension: mock_call_context = mock.MagicMock(operation=operation, params={}) return _LambdaExtension(mock_call_context) - @mock_lambda + @mock_aws def test_list_functions(self): self.client.list_functions() self.assert_span("ListFunctions") - @mock_iam + @mock_aws def _create_role_and_get_arn(self) -> str: return self.iam_client.create_role( RoleName="my-role", @@ -131,7 +131,7 @@ def _create_lambda_function(self, function_name: str, function_code: str): sys.platform == "win32", reason="requires docker and Github CI Windows does not have docker installed by default", ) - @mock_lambda + @mock_aws def test_invoke(self): previous_propagator = get_global_textmap() try: diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sns.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sns.py index e2b4c55732..5d6b94f145 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sns.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sns.py @@ -18,7 +18,7 @@ import botocore.session from botocore.awsrequest import AWSResponse -from moto import mock_sns +from moto import mock_aws from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.semconv.trace import ( @@ -91,11 +91,11 @@ def assert_injected_span(self, message_attrs: Dict[str, Any], span: Span): self.assertEqual(span_context.trace_id, int(trace_parent[1], 16)) self.assertEqual(span_context.span_id, int(trace_parent[2], 16)) - @mock_sns + @mock_aws def test_publish_to_topic_arn(self): self._test_publish_to_arn("TopicArn") - @mock_sns + @mock_aws def test_publish_to_target_arn(self): self._test_publish_to_arn("TargetArn") @@ -125,7 +125,7 @@ def _test_publish_to_arn(self, arg_name: str): span.attributes["messaging.destination.name"], ) - @mock_sns + @mock_aws def test_publish_to_phone_number(self): phone_number = "+10000000000" self.client.publish( @@ -138,7 +138,7 @@ def test_publish_to_phone_number(self): phone_number, span.attributes[SpanAttributes.MESSAGING_DESTINATION] ) - @mock_sns + @mock_aws def test_publish_injects_span(self): message_attrs = {} topic_arn = self._create_topic() diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sqs.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sqs.py index 6bcffd9274..cdf39e4ece 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sqs.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sqs.py @@ -1,5 +1,5 @@ import botocore.session -from moto import mock_sqs +from moto import mock_aws from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.semconv.trace import SpanAttributes @@ -22,7 +22,7 @@ def tearDown(self): super().tearDown() BotocoreInstrumentor().uninstrument() - @mock_sqs + @mock_aws def test_sqs_messaging_send_message(self): create_queue_result = self.client.create_queue( QueueName="test_queue_name" @@ -51,7 +51,7 @@ def test_sqs_messaging_send_message(self): response["MessageId"], ) - @mock_sqs + @mock_aws def test_sqs_messaging_send_message_batch(self): create_queue_result = self.client.create_queue( QueueName="test_queue_name" @@ -85,7 +85,7 @@ def test_sqs_messaging_send_message_batch(self): response["Successful"][0]["MessageId"], ) - @mock_sqs + @mock_aws def test_sqs_messaging_receive_message(self): create_queue_result = self.client.create_queue( QueueName="test_queue_name" @@ -116,7 +116,7 @@ def test_sqs_messaging_receive_message(self): message_result["Messages"][0]["MessageId"], ) - @mock_sqs + @mock_aws def test_sqs_messaging_failed_operation(self): with self.assertRaises(Exception): self.client.send_message( diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/pyproject.toml b/instrumentation/opentelemetry-instrumentation-confluent-kafka/pyproject.toml index cac767986e..94a2497b36 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ [project.optional-dependencies] instruments = [ - "confluent-kafka >= 1.8.2, <= 2.3.0", + "confluent-kafka >= 1.8.2, <= 2.4.0", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/__init__.py b/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/__init__.py index 30181d39c2..45d45ccb63 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/__init__.py @@ -156,6 +156,9 @@ def flush(self, timeout=-1): def poll(self, timeout=-1): return self._producer.poll(timeout) + def purge(self, in_queue=True, in_flight=True, blocking=True): + self._producer.purge(in_queue, in_flight, blocking) + def produce( self, topic, value=None, *args, **kwargs ): # pylint: disable=keyword-arg-before-vararg diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/package.py b/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/package.py index bbc900c1dd..6ebddd30ac 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/package.py +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/package.py @@ -13,4 +13,4 @@ # limitations under the License. -_instruments = ("confluent-kafka >= 1.8.2, <= 2.3.0",) +_instruments = ("confluent-kafka >= 1.8.2, <= 2.4.0",) diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-confluent-kafka/test-requirements.txt index 1297185d4b..87387ded81 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/test-requirements.txt @@ -1,5 +1,5 @@ asgiref==3.7.2 -confluent-kafka==2.3.0 +confluent-kafka==2.4.0 Deprecated==1.2.14 importlib-metadata==6.11.0 iniconfig==2.0.0 diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-0.txt index dac65a0f01..e6d9bb6f9d 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-0.txt +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-0.txt @@ -13,7 +13,7 @@ python-dateutil==2.8.2 six==1.16.0 tomli==2.0.1 typing_extensions==4.10.0 -urllib3==1.26.18 +urllib3==1.26.19 wrapt==1.16.0 zipp==3.17.0 -e opentelemetry-instrumentation diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-1.txt index c9baa38ad6..12e3a1c229 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-1.txt +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-1.txt @@ -13,7 +13,7 @@ python-dateutil==2.8.2 six==1.16.0 tomli==2.0.1 typing_extensions==4.10.0 -urllib3==1.26.18 +urllib3==1.26.19 wrapt==1.16.0 zipp==3.17.0 -e opentelemetry-instrumentation diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt index b852eff7cb..f34d67d9c8 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt @@ -14,7 +14,7 @@ python-dateutil==2.8.2 six==1.16.0 tomli==2.0.1 typing_extensions==4.10.0 -urllib3==2.2.1 +urllib3==2.2.2 wrapt==1.16.0 zipp==3.17.0 -e opentelemetry-instrumentation diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py index 263cc0fb78..4c673d214a 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py @@ -115,7 +115,7 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A single item list containing all the header values. For example: -``http.request.header.custom_request_header = [","]`` +``http.request.header.custom_request_header = ["", ""]`` Response headers **************** @@ -146,10 +146,10 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a -single item list containing all the header values. +list containing the header values. For example: -``http.response.header.custom_response_header = [","]`` +``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.txt index c73b0ed688..2116980b3f 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.txt @@ -24,7 +24,7 @@ sniffio==1.3.0 starlette==0.36.3 tomli==2.0.1 typing_extensions==4.9.0 -urllib3==2.2.1 +urllib3==2.2.2 wrapt==1.16.0 zipp==3.17.0 -e opentelemetry-instrumentation diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index a7cd5045ee..26d9e743a8 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -11,9 +11,10 @@ # 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 collections.abc import Mapping from timeit import default_timer +from typing import Tuple from unittest.mock import patch import fastapi @@ -557,6 +558,24 @@ def test_mark_span_internal_in_presence_of_span_from_other_framework(self): ) +class MultiMapping(Mapping): + + def __init__(self, *items: Tuple[str, str]): + self._items = items + + def __len__(self): + return len(self._items) + + def __getitem__(self, __key): + raise NotImplementedError("use .items() instead") + + def __iter__(self): + raise NotImplementedError("use .items() instead") + + def items(self): + return self._items + + @patch.dict( "os.environ", { @@ -583,13 +602,15 @@ def _create_app(): @app.get("/foobar") async def _(): - headers = { - "custom-test-header-1": "test-header-value-1", - "custom-test-header-2": "test-header-value-2", - "my-custom-regex-header-1": "my-custom-regex-value-1,my-custom-regex-value-2", - "My-Custom-Regex-Header-2": "my-custom-regex-value-3,my-custom-regex-value-4", - "My-Secret-Header": "My Secret Value", - } + headers = MultiMapping( + ("custom-test-header-1", "test-header-value-1"), + ("custom-test-header-2", "test-header-value-2"), + ("my-custom-regex-header-1", "my-custom-regex-value-1"), + ("my-custom-regex-header-1", "my-custom-regex-value-2"), + ("My-Custom-Regex-Header-2", "my-custom-regex-value-3"), + ("My-Custom-Regex-Header-2", "my-custom-regex-value-4"), + ("My-Secret-Header", "My Secret Value"), + ) content = {"message": "hello world"} return JSONResponse(content=content, headers=headers) @@ -665,10 +686,12 @@ def test_http_custom_response_headers_in_span_attributes(self): "test-header-value-2", ), "http.response.header.my_custom_regex_header_1": ( - "my-custom-regex-value-1,my-custom-regex-value-2", + "my-custom-regex-value-1", + "my-custom-regex-value-2", ), "http.response.header.my_custom_regex_header_2": ( - "my-custom-regex-value-3,my-custom-regex-value-4", + "my-custom-regex-value-3", + "my-custom-regex-value-4", ), "http.response.header.my_secret_header": ("[REDACTED]",), } diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py index f2e0ee34cc..34e9b5ea50 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py @@ -251,7 +251,6 @@ def response_hook(span: Span, status: str, response_headers: List): import opentelemetry.instrumentation.wsgi as otel_wsgi from opentelemetry import context, trace from opentelemetry.instrumentation._semconv import ( - _METRIC_ATTRIBUTES_SERVER_DURATION_NAME, _get_schema_url, _HTTPStabilityMode, _OpenTelemetrySemanticConventionStability, @@ -268,6 +267,9 @@ def response_hook(span: Span, status: str, response_headers: List): from opentelemetry.instrumentation.utils import _start_internal_or_server_span from opentelemetry.metrics import get_meter from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.semconv.metrics.http_metrics import ( + HTTP_SERVER_REQUEST_DURATION, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.util.http import ( get_excluded_urls, @@ -553,7 +555,7 @@ def __init__(self, *args, **kwargs): duration_histogram_new = None if _report_new(_InstrumentedFlask._sem_conv_opt_in_mode): duration_histogram_new = meter.create_histogram( - name=_METRIC_ATTRIBUTES_SERVER_DURATION_NAME, + name=HTTP_SERVER_REQUEST_DURATION, unit="s", description="measures the duration of the inbound HTTP request", ) @@ -684,7 +686,7 @@ def instrument_app( duration_histogram_new = None if _report_new(sem_conv_opt_in_mode): duration_histogram_new = meter.create_histogram( - name=_METRIC_ATTRIBUTES_SERVER_DURATION_NAME, + name=HTTP_SERVER_REQUEST_DURATION, unit="s", description="measures the duration of the inbound HTTP request", ) diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py index d30a100b0e..f50d3245a0 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py @@ -20,7 +20,6 @@ from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( - _SPAN_ATTRIBUTES_ERROR_TYPE, OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, _server_active_requests_count_attrs_new, @@ -40,6 +39,7 @@ NumberDataPoint, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.util.http import ( @@ -379,7 +379,7 @@ def test_internal_error_new_semconv(self): SpanAttributes.URL_PATH: "/hello/500", SpanAttributes.HTTP_ROUTE: "/hello/", SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 500, - _SPAN_ATTRIBUTES_ERROR_TYPE: "500", + ERROR_TYPE: "500", SpanAttributes.URL_SCHEME: "http", } ) @@ -405,7 +405,7 @@ def test_internal_error_both_semconv(self): { SpanAttributes.URL_PATH: "/hello/500", SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 500, - _SPAN_ATTRIBUTES_ERROR_TYPE: "500", + ERROR_TYPE: "500", SpanAttributes.URL_SCHEME: "http", } ) diff --git a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py index 850e76eea3..5404b2f025 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py @@ -196,11 +196,13 @@ async def async_response_hook(span, request, response): import httpx -from opentelemetry import context from opentelemetry.instrumentation.httpx.package import _instruments from opentelemetry.instrumentation.httpx.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.utils import http_status_to_status_code +from opentelemetry.instrumentation.utils import ( + http_status_to_status_code, + is_http_instrumentation_enabled, +) from opentelemetry.propagate import inject from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import SpanKind, TracerProvider, get_tracer @@ -347,7 +349,7 @@ def handle_request( httpx.Response, ]: """Add request info to span.""" - if context.get_value("suppress_instrumentation"): + if not is_http_instrumentation_enabled(): return self._transport.handle_request(*args, **kwargs) method, url, headers, stream, extensions = _extract_parameters( @@ -438,7 +440,7 @@ async def handle_async_request(self, *args, **kwargs) -> typing.Union[ httpx.Response, ]: """Add request info to span.""" - if context.get_value("suppress_instrumentation"): + if not is_http_instrumentation_enabled(): return await self._transport.handle_async_request(*args, **kwargs) method, url, headers, stream, extensions = _extract_parameters( diff --git a/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py b/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py index d64db1a8f5..06ad963ab0 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py +++ b/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py @@ -21,12 +21,13 @@ import respx import opentelemetry.instrumentation.httpx -from opentelemetry import context, trace +from opentelemetry import trace from opentelemetry.instrumentation.httpx import ( AsyncOpenTelemetryTransport, HTTPXClientInstrumentor, SyncOpenTelemetryTransport, ) +from opentelemetry.instrumentation.utils import suppress_http_instrumentation from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.sdk import resources from opentelemetry.semconv.trace import SpanAttributes @@ -191,14 +192,9 @@ def test_not_foundbasic(self): ) def test_suppress_instrumentation(self): - token = context.attach( - context.set_value("suppress_instrumentation", True) - ) - try: + with suppress_http_instrumentation(): result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") - finally: - context.detach(token) self.assert_span(num_spans=0) @@ -512,15 +508,10 @@ def test_not_recording(self): def test_suppress_instrumentation_new_client(self): HTTPXClientInstrumentor().instrument() - token = context.attach( - context.set_value("suppress_instrumentation", True) - ) - try: + with suppress_http_instrumentation(): client = self.create_client() result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") - finally: - context.detach(token) self.assert_span(num_spans=0) HTTPXClientInstrumentor().uninstrument() diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt index 9a1076a543..5f2d8b7783 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt @@ -5,7 +5,7 @@ iniconfig==2.0.0 packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 -PyMySQL==1.1.0 +PyMySQL==1.1.1 pytest==7.4.4 pytest-benchmark==4.0.0 tomli==2.0.1 diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py b/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py index 587ebc1b53..6f8af5d8df 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py @@ -17,6 +17,7 @@ import pymysql import opentelemetry.instrumentation.pymysql +from opentelemetry import trace as trace_api from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor from opentelemetry.sdk import resources from opentelemetry.test.test_base import TestBase @@ -78,6 +79,20 @@ def test_custom_tracer_provider(self, mock_connect): self.assertIs(span.resource, resource) + @mock.patch("pymysql.connect") + # pylint: disable=unused-argument + def test_no_op_tracer_provider(self, mock_connect): + PyMySQLInstrumentor().instrument( + tracer_provider=trace_api.NoOpTracerProvider() + ) + cnx = pymysql.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 0) + @mock.patch("pymysql.connect") # pylint: disable=unused-argument def test_instrument_connection(self, mock_connect): diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py index ede3e09608..d0010ed8d0 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py @@ -31,6 +31,7 @@ from opentelemetry.metrics import get_meter from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.http import get_excluded_urls TWEEN_NAME = "opentelemetry.instrumentation.pyramid.trace_tween_factory" @@ -180,6 +181,7 @@ def trace_tween(request): response = None status = None + recordable_exc = None try: response = handler(request) @@ -190,11 +192,14 @@ def trace_tween(request): # As described in docs, Pyramid exceptions are all valid # response types response = exc + if isinstance(exc, HTTPServerError): + recordable_exc = exc raise - except BaseException: + except BaseException as exc: # In the case that a non-HTTPException is bubbled up we # should infer a internal server error and raise status = "500 InternalServerError" + recordable_exc = exc raise finally: duration = max(round((default_timer() - start) * 1000), 0) @@ -222,6 +227,12 @@ def trace_tween(request): getattr(response, "headerlist", None), ) + if recordable_exc is not None: + span.set_status( + Status(StatusCode.ERROR, str(recordable_exc)) + ) + span.record_exception(recordable_exc) + if span.is_recording() and span.kind == trace.SpanKind.SERVER: custom_attributes = ( otel_wsgi.collect_custom_response_headers_attributes( diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt index 3edddf72c3..184b03fed4 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt @@ -17,7 +17,7 @@ translationstring==1.4 typing_extensions==4.9.0 venusian==3.1.0 WebOb==1.8.7 -Werkzeug==0.16.1 +Werkzeug==3.0.3 wrapt==1.16.0 zipp==3.17.0 zope.deprecation==5.0 diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py index c6b9faa196..9c177433ef 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py @@ -14,12 +14,7 @@ import pyramid.httpexceptions as exc from pyramid.response import Response -from werkzeug.test import Client - -# opentelemetry-instrumentation-pyramid uses werkzeug==0.16.1 which has -# werkzeug.wrappers.BaseResponse. This is not the case for newer versions of -# werkzeug like the one lint uses. -from werkzeug.wrappers import BaseResponse # pylint: disable=no-name-in-module +from werkzeug.test import Client, TestResponse class InstrumentationTest: @@ -35,7 +30,7 @@ def _hello_endpoint(request): if helloid == 204: raise exc.HTTPNoContent() if helloid == 900: - raise NotImplementedError() + raise NotImplementedError("error message") return Response("Hello: " + str(helloid)) @staticmethod @@ -77,4 +72,4 @@ def excluded2_endpoint(request): ) # pylint: disable=attribute-defined-outside-init - self.client = Client(config.make_wsgi_app(), BaseResponse) + self.client = Client(config.make_wsgi_app(), TestResponse) diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py index 7b48e16e17..b1d854b371 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py @@ -121,6 +121,7 @@ def test_redirect_response_is_not_an_error(self): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].status.status_code, StatusCode.UNSET) + self.assertEqual(len(span_list[0].events), 0) PyramidInstrumentor().uninstrument() diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py index c566c301d8..0e3a5dec19 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py @@ -23,6 +23,7 @@ set_global_response_propagator, ) from opentelemetry.instrumentation.pyramid import PyramidInstrumentor +from opentelemetry.semconv.attributes import exception_attributes from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.util.http import get_excluded_urls @@ -149,6 +150,7 @@ def test_404(self): self.assertEqual(span_list[0].name, "POST /bye") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) + self.assertEqual(len(span_list[0].events), 0) def test_internal_error(self): expected_attrs = expected_attributes( @@ -166,6 +168,18 @@ def test_internal_error(self): self.assertEqual(span_list[0].name, "/hello/{helloid}") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) + self.assertEqual( + span_list[0].status.status_code, trace.StatusCode.ERROR + ) + self.assertIn( + "HTTPInternalServerError", span_list[0].status.description + ) + self.assertEqual( + span_list[0] + .events[0] + .attributes[exception_attributes.EXCEPTION_TYPE], + "pyramid.httpexceptions.HTTPInternalServerError", + ) def test_internal_exception(self): expected_attrs = expected_attributes( @@ -184,6 +198,21 @@ def test_internal_exception(self): self.assertEqual(span_list[0].name, "/hello/{helloid}") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) + self.assertEqual( + span_list[0].status.status_code, trace.StatusCode.ERROR + ) + self.assertEqual(span_list[0].status.description, "error message") + + expected_error_event_attrs = { + exception_attributes.EXCEPTION_TYPE: "NotImplementedError", + exception_attributes.EXCEPTION_MESSAGE: "error message", + } + self.assertEqual(span_list[0].events[0].name, "exception") + # Ensure exception event has specific attributes, but allow additional ones + self.assertLess( + expected_error_event_attrs.items(), + dict(span_list[0].events[0].attributes).items(), + ) def test_tween_list(self): tween_list = "opentelemetry.instrumentation.pyramid.trace_tween_factory\npyramid.tweens.excview_tween_factory" diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py index 8c54482a46..18cc3e767c 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py @@ -59,10 +59,6 @@ from requests.structures import CaseInsensitiveDict from opentelemetry.instrumentation._semconv import ( - _METRIC_ATTRIBUTES_CLIENT_DURATION_NAME, - _SPAN_ATTRIBUTES_ERROR_TYPE, - _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS, - _SPAN_ATTRIBUTES_NETWORK_PEER_PORT, _client_duration_attrs_new, _client_duration_attrs_old, _filter_semconv_duration_attrs, @@ -91,7 +87,15 @@ ) from opentelemetry.metrics import Histogram, get_meter from opentelemetry.propagate import inject +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE +from opentelemetry.semconv.attributes.network_attributes import ( + NETWORK_PEER_ADDRESS, + NETWORK_PEER_PORT, +) from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.semconv.metrics.http_metrics import ( + HTTP_CLIENT_REQUEST_DURATION, +) from opentelemetry.trace import SpanKind, Tracer, get_tracer from opentelemetry.trace.span import Span from opentelemetry.trace.status import StatusCode @@ -191,9 +195,7 @@ def get_or_create_headers(): sem_conv_opt_in_mode, ) # Use semconv library when available - span_attributes[_SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS] = ( - parsed_url.hostname - ) + span_attributes[NETWORK_PEER_ADDRESS] = parsed_url.hostname if parsed_url.port: _set_http_peer_port_client( metric_labels, parsed_url.port, sem_conv_opt_in_mode @@ -203,9 +205,7 @@ def get_or_create_headers(): span_attributes, parsed_url.port, sem_conv_opt_in_mode ) # Use semconv library when available - span_attributes[_SPAN_ATTRIBUTES_NETWORK_PEER_PORT] = ( - parsed_url.port - ) + span_attributes[NETWORK_PEER_PORT] = parsed_url.port except ValueError: pass @@ -250,12 +250,8 @@ def get_or_create_headers(): _report_new(sem_conv_opt_in_mode) and status_code is StatusCode.ERROR ): - span_attributes[_SPAN_ATTRIBUTES_ERROR_TYPE] = str( - result.status_code - ) - metric_labels[_SPAN_ATTRIBUTES_ERROR_TYPE] = str( - result.status_code - ) + span_attributes[ERROR_TYPE] = str(result.status_code) + metric_labels[ERROR_TYPE] = str(result.status_code) if result.raw is not None: version = getattr(result.raw, "version", None) @@ -278,12 +274,8 @@ def get_or_create_headers(): response_hook(span, request, result) if exception is not None and _report_new(sem_conv_opt_in_mode): - span.set_attribute( - _SPAN_ATTRIBUTES_ERROR_TYPE, type(exception).__qualname__ - ) - metric_labels[_SPAN_ATTRIBUTES_ERROR_TYPE] = type( - exception - ).__qualname__ + span.set_attribute(ERROR_TYPE, type(exception).__qualname__) + metric_labels[ERROR_TYPE] = type(exception).__qualname__ if duration_histogram_old is not None: duration_attrs_old = _filter_semconv_duration_attrs( @@ -403,7 +395,7 @@ def _instrument(self, **kwargs): duration_histogram_new = None if _report_new(semconv_opt_in_mode): duration_histogram_new = meter.create_histogram( - name=_METRIC_ATTRIBUTES_CLIENT_DURATION_NAME, + name=HTTP_CLIENT_REQUEST_DURATION, unit="s", description="Duration of HTTP client requests.", ) diff --git a/instrumentation/opentelemetry-instrumentation-requests/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-requests/test-requirements.txt index 1270d12eb1..c5c8f84366 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-requests/test-requirements.txt @@ -14,7 +14,7 @@ pytest-benchmark==4.0.0 requests==2.32.3 tomli==2.0.1 typing_extensions==4.9.0 -urllib3==2.2.1 +urllib3==2.2.2 wrapt==1.16.0 zipp==3.17.0 -e opentelemetry-instrumentation diff --git a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py index d85d70e20e..75518fc8d3 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py +++ b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py @@ -23,9 +23,6 @@ import opentelemetry.instrumentation.requests from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( - _SPAN_ATTRIBUTES_ERROR_TYPE, - _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS, - _SPAN_ATTRIBUTES_NETWORK_PEER_PORT, OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) @@ -36,6 +33,21 @@ ) from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.sdk import resources +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_STATUS_CODE, +) +from opentelemetry.semconv.attributes.network_attributes import ( + NETWORK_PEER_ADDRESS, + NETWORK_PEER_PORT, + NETWORK_PROTOCOL_VERSION, +) +from opentelemetry.semconv.attributes.server_attributes import ( + SERVER_ADDRESS, + SERVER_PORT, +) +from opentelemetry.semconv.attributes.url_attributes import URL_FULL from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.mock_textmap import MockTextMapPropagator from opentelemetry.test.test_base import TestBase @@ -176,14 +188,14 @@ def test_basic_new_semconv(self): self.assertEqual( span.attributes, { - SpanAttributes.HTTP_REQUEST_METHOD: "GET", - SpanAttributes.URL_FULL: url_with_port, - SpanAttributes.SERVER_ADDRESS: "mock", - _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS: "mock", - SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200, - SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1", - SpanAttributes.SERVER_PORT: 80, - _SPAN_ATTRIBUTES_NETWORK_PEER_PORT: 80, + HTTP_REQUEST_METHOD: "GET", + URL_FULL: url_with_port, + SERVER_ADDRESS: "mock", + NETWORK_PEER_ADDRESS: "mock", + HTTP_RESPONSE_STATUS_CODE: 200, + NETWORK_PROTOCOL_VERSION: "1.1", + SERVER_PORT: 80, + NETWORK_PEER_PORT: 80, }, ) @@ -213,19 +225,19 @@ def test_basic_both_semconv(self): span.attributes, { SpanAttributes.HTTP_METHOD: "GET", - SpanAttributes.HTTP_REQUEST_METHOD: "GET", + HTTP_REQUEST_METHOD: "GET", SpanAttributes.HTTP_URL: url_with_port, - SpanAttributes.URL_FULL: url_with_port, + URL_FULL: url_with_port, SpanAttributes.HTTP_HOST: "mock", - SpanAttributes.SERVER_ADDRESS: "mock", - _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS: "mock", + SERVER_ADDRESS: "mock", + NETWORK_PEER_ADDRESS: "mock", SpanAttributes.NET_PEER_PORT: 80, SpanAttributes.HTTP_STATUS_CODE: 200, - SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200, + HTTP_RESPONSE_STATUS_CODE: 200, SpanAttributes.HTTP_FLAVOR: "1.1", - SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1", - SpanAttributes.SERVER_PORT: 80, - _SPAN_ATTRIBUTES_NETWORK_PEER_PORT: 80, + NETWORK_PROTOCOL_VERSION: "1.1", + SERVER_PORT: 80, + NETWORK_PEER_PORT: 80, }, ) @@ -328,12 +340,8 @@ def test_not_foundbasic_new_semconv(self): span = self.assert_span() - self.assertEqual( - span.attributes.get(SpanAttributes.HTTP_RESPONSE_STATUS_CODE), 404 - ) - self.assertEqual( - span.attributes.get(_SPAN_ATTRIBUTES_ERROR_TYPE), "404" - ) + self.assertEqual(span.attributes.get(HTTP_RESPONSE_STATUS_CODE), 404) + self.assertEqual(span.attributes.get(ERROR_TYPE), "404") self.assertIs( span.status.status_code, @@ -355,12 +363,8 @@ def test_not_foundbasic_both_semconv(self): self.assertEqual( span.attributes.get(SpanAttributes.HTTP_STATUS_CODE), 404 ) - self.assertEqual( - span.attributes.get(SpanAttributes.HTTP_RESPONSE_STATUS_CODE), 404 - ) - self.assertEqual( - span.attributes.get(_SPAN_ATTRIBUTES_ERROR_TYPE), "404" - ) + self.assertEqual(span.attributes.get(HTTP_RESPONSE_STATUS_CODE), 404) + self.assertEqual(span.attributes.get(ERROR_TYPE), "404") self.assertIs( span.status.status_code, @@ -527,13 +531,13 @@ def test_requests_exception_new_semconv(self, *_, **__): self.assertEqual( span.attributes, { - SpanAttributes.HTTP_REQUEST_METHOD: "GET", - SpanAttributes.URL_FULL: url_with_port, - SpanAttributes.SERVER_ADDRESS: "mock", - SpanAttributes.SERVER_PORT: 80, - _SPAN_ATTRIBUTES_NETWORK_PEER_PORT: 80, - _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS: "mock", - _SPAN_ATTRIBUTES_ERROR_TYPE: "RequestException", + HTTP_REQUEST_METHOD: "GET", + URL_FULL: url_with_port, + SERVER_ADDRESS: "mock", + SERVER_PORT: 80, + NETWORK_PEER_PORT: 80, + NETWORK_PEER_ADDRESS: "mock", + ERROR_TYPE: "RequestException", }, ) self.assertEqual(span.status.status_code, StatusCode.ERROR) @@ -724,11 +728,11 @@ def test_basic_metric_new_semconv(self): self.perform_request(self.URL) expected_attributes = { - SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200, - SpanAttributes.SERVER_ADDRESS: "examplehost", - SpanAttributes.SERVER_PORT: 8000, - SpanAttributes.HTTP_REQUEST_METHOD: "GET", - SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1", + HTTP_RESPONSE_STATUS_CODE: 200, + SERVER_ADDRESS: "examplehost", + SERVER_PORT: 8000, + HTTP_REQUEST_METHOD: "GET", + NETWORK_PROTOCOL_VERSION: "1.1", } for ( resource_metrics @@ -760,11 +764,11 @@ def test_basic_metric_both_semconv(self): } expected_attributes_new = { - SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200, - SpanAttributes.SERVER_ADDRESS: "examplehost", - SpanAttributes.SERVER_PORT: 8000, - SpanAttributes.HTTP_REQUEST_METHOD: "GET", - SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1", + HTTP_RESPONSE_STATUS_CODE: 200, + SERVER_ADDRESS: "examplehost", + SERVER_PORT: 8000, + HTTP_REQUEST_METHOD: "GET", + NETWORK_PROTOCOL_VERSION: "1.1", } for ( diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py index 4bb3608935..474a942a98 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py @@ -110,10 +110,10 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a -single item list containing all the header values. +list containing the header values. For example: -``http.request.header.custom_request_header = [","]`` +``http.request.header.custom_request_header = ["", ""]`` Response headers **************** @@ -144,10 +144,10 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a -single item list containing all the header values. +list containing the header values. For example: -``http.response.header.custom_response_header = [","]`` +``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** diff --git a/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.txt index 882f0e165a..a582353901 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.txt @@ -20,7 +20,7 @@ sniffio==1.3.0 starlette==0.13.8 tomli==2.0.1 typing_extensions==4.9.0 -urllib3==2.2.1 +urllib3==2.2.2 wrapt==1.16.0 zipp==3.17.0 -e opentelemetry-instrumentation diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml b/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml index 07ba2faa20..8518ddc8db 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ dependencies = [ "opentelemetry-instrumentation == 0.47b0.dev", "opentelemetry-api ~= 1.11", - "psutil ~= 5.9", + "psutil >= 5.9.0, < 7", ] [project.optional-dependencies] diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-system-metrics/test-requirements.txt index ddb98399cf..13573c65bd 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/test-requirements.txt @@ -4,7 +4,7 @@ importlib-metadata==6.11.0 iniconfig==2.0.0 packaging==24.0 pluggy==1.5.0 -psutil==5.9.8 +psutil==6.0.0 py-cpuinfo==9.0.0 pytest==7.4.4 pytest-benchmark==4.0.0 diff --git a/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt index 86ef01b096..14ac028083 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt @@ -19,9 +19,9 @@ pytest==7.4.4 pytest-benchmark==4.0.0 requests==2.32.3 tomli==2.0.1 -tornado==6.4 +tornado==6.4.1 typing_extensions==4.9.0 -urllib3==2.2.1 +urllib3==2.2.2 Werkzeug==3.0.3 wrapt==1.16.0 zipp==3.17.0 diff --git a/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-0.txt index 05a76b1bcb..6eb6272dbf 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-0.txt +++ b/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-0.txt @@ -10,7 +10,7 @@ pytest==7.4.4 pytest-benchmark==4.0.0 tomli==2.0.1 typing_extensions==4.10.0 -urllib3==1.26.18 +urllib3==1.26.19 wrapt==1.16.0 zipp==3.17.0 -e opentelemetry-instrumentation diff --git a/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt index 9c6d596068..402beb85b9 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt +++ b/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt @@ -10,7 +10,7 @@ pytest==7.4.4 pytest-benchmark==4.0.0 tomli==2.0.1 typing_extensions==4.10.0 -urllib3==2.2.1 +urllib3==2.2.2 wrapt==1.16.0 zipp==3.17.0 -e opentelemetry-instrumentation diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py index 810a07e315..6a1883fa7e 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py @@ -214,8 +214,6 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he from opentelemetry import context, trace from opentelemetry.instrumentation._semconv import ( - _METRIC_ATTRIBUTES_SERVER_DURATION_NAME, - _SPAN_ATTRIBUTES_ERROR_TYPE, _filter_semconv_active_request_count_attr, _filter_semconv_duration_attrs, _get_schema_url, @@ -244,7 +242,11 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he from opentelemetry.instrumentation.wsgi.version import __version__ from opentelemetry.metrics import get_meter from opentelemetry.propagators.textmap import Getter +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.semconv.metrics.http_metrics import ( + HTTP_SERVER_REQUEST_DURATION, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.http import ( @@ -573,7 +575,7 @@ def __init__( self.duration_histogram_new = None if _report_new(sem_conv_opt_in_mode): self.duration_histogram_new = self.meter.create_histogram( - name=_METRIC_ATTRIBUTES_SERVER_DURATION_NAME, + name=HTTP_SERVER_REQUEST_DURATION, unit="s", description="measures the duration of the inbound HTTP request", ) @@ -670,11 +672,9 @@ def __call__(self, environ, start_response): return _end_span_after_iterating(iterable, span, token) except Exception as ex: if _report_new(self._sem_conv_opt_in_mode): - req_attrs[_SPAN_ATTRIBUTES_ERROR_TYPE] = type(ex).__qualname__ + req_attrs[ERROR_TYPE] = type(ex).__qualname__ if span.is_recording(): - span.set_attribute( - _SPAN_ATTRIBUTES_ERROR_TYPE, type(ex).__qualname__ - ) + span.set_attribute(ERROR_TYPE, type(ex).__qualname__) span.set_status(Status(StatusCode.ERROR, str(ex))) span.end() if token is not None: diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py index 2b26cbb5f9..777d19f41d 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py @@ -36,6 +36,23 @@ NumberDataPoint, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_STATUS_CODE, +) +from opentelemetry.semconv.attributes.network_attributes import ( + NETWORK_PROTOCOL_VERSION, +) +from opentelemetry.semconv.attributes.server_attributes import ( + SERVER_ADDRESS, + SERVER_PORT, +) +from opentelemetry.semconv.attributes.url_attributes import ( + URL_FULL, + URL_PATH, + URL_QUERY, + URL_SCHEME, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase @@ -237,11 +254,11 @@ def validate_response( SpanAttributes.NET_HOST_NAME: "127.0.0.1", } expected_attributes_new = { - SpanAttributes.SERVER_PORT: 80, - SpanAttributes.SERVER_ADDRESS: "127.0.0.1", - SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.0", - SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200, - SpanAttributes.URL_SCHEME: "http", + SERVER_PORT: 80, + SERVER_ADDRESS: "127.0.0.1", + NETWORK_PROTOCOL_VERSION: "1.0", + HTTP_RESPONSE_STATUS_CODE: 200, + URL_SCHEME: "http", } if old_sem_conv: expected_attributes.update(expected_attributes_old) @@ -253,9 +270,7 @@ def validate_response( if old_sem_conv: expected_attributes[SpanAttributes.HTTP_METHOD] = http_method if new_sem_conv: - expected_attributes[SpanAttributes.HTTP_REQUEST_METHOD] = ( - http_method - ) + expected_attributes[HTTP_REQUEST_METHOD] = http_method self.assertEqual(span_list[0].attributes, expected_attributes) def test_basic_wsgi_call(self): @@ -517,13 +532,13 @@ def test_request_attributes_new_semconv(self): self.assertDictEqual( attrs, { - SpanAttributes.HTTP_REQUEST_METHOD: "GET", - SpanAttributes.SERVER_ADDRESS: "127.0.0.1", - SpanAttributes.SERVER_PORT: 80, - SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.0", - SpanAttributes.URL_PATH: "/", - SpanAttributes.URL_QUERY: "foo=bar", - SpanAttributes.URL_SCHEME: "http", + HTTP_REQUEST_METHOD: "GET", + SERVER_ADDRESS: "127.0.0.1", + SERVER_PORT: 80, + NETWORK_PROTOCOL_VERSION: "1.0", + URL_PATH: "/", + URL_QUERY: "foo=bar", + URL_SCHEME: "http", }, ) @@ -543,11 +558,10 @@ def validate_url( SpanAttributes.HTTP_SERVER_NAME: parts.hostname, # Not true in the general case, but for all tests. } expected_new = { - SpanAttributes.SERVER_PORT: parts.port - or (80 if parts.scheme == "http" else 443), - SpanAttributes.SERVER_ADDRESS: parts.hostname, - SpanAttributes.URL_PATH: parts.path, - SpanAttributes.URL_QUERY: parts.query, + SERVER_PORT: parts.port or (80 if parts.scheme == "http" else 443), + SERVER_ADDRESS: parts.hostname, + URL_PATH: parts.path, + URL_QUERY: parts.query, } if old_semconv: if raw: @@ -560,17 +574,15 @@ def validate_url( expected_old[SpanAttributes.HTTP_HOST] = parts.hostname if new_semconv: if raw: - expected_new[SpanAttributes.URL_PATH] = expected_url.split( - parts.path, 1 - )[1] + expected_new[URL_PATH] = expected_url.split(parts.path, 1)[1] if parts.query: - expected_new[SpanAttributes.URL_QUERY] = ( - expected_url.split(parts.query, 1)[1] - ) + expected_new[URL_QUERY] = expected_url.split( + parts.query, 1 + )[1] else: - expected_new[SpanAttributes.HTTP_URL] = expected_url + expected_new[URL_FULL] = expected_url if has_host: - expected_new[SpanAttributes.SERVER_ADDRESS] = parts.hostname + expected_new[SERVER_ADDRESS] = parts.hostname attrs = otel_wsgi.collect_request_attributes(self.environ) self.assertGreaterEqual( @@ -720,8 +732,8 @@ def test_request_attributes_with_full_request_uri(self): SpanAttributes.HTTP_TARGET: "http://docs.python.org:80/3/library/urllib.parse.html?highlight=params#url-parsing", } expected_new = { - SpanAttributes.URL_PATH: "/3/library/urllib.parse.html", - SpanAttributes.URL_QUERY: "highlight=params", + URL_PATH: "/3/library/urllib.parse.html", + URL_QUERY: "highlight=params", } self.assertGreaterEqual( otel_wsgi.collect_request_attributes(self.environ).items(), diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index efe3c75f70..baa06ff99b 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -17,16 +17,27 @@ from enum import Enum from opentelemetry.instrumentation.utils import http_status_to_status_code +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_REQUEST_METHOD, + HTTP_REQUEST_METHOD_ORIGINAL, + HTTP_RESPONSE_STATUS_CODE, + HTTP_ROUTE, +) +from opentelemetry.semconv.attributes.network_attributes import ( + NETWORK_PROTOCOL_VERSION, +) +from opentelemetry.semconv.attributes.server_attributes import ( + SERVER_ADDRESS, + SERVER_PORT, +) +from opentelemetry.semconv.attributes.url_attributes import ( + URL_FULL, + URL_SCHEME, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace.status import Status, StatusCode -# TODO: will come through semconv package once updated -_SPAN_ATTRIBUTES_ERROR_TYPE = "error.type" -_SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS = "network.peer.address" -_SPAN_ATTRIBUTES_NETWORK_PEER_PORT = "network.peer.port" -_METRIC_ATTRIBUTES_CLIENT_DURATION_NAME = "http.client.request.duration" -_METRIC_ATTRIBUTES_SERVER_DURATION_NAME = "http.server.request.duration" - _client_duration_attrs_old = [ SpanAttributes.HTTP_STATUS_CODE, SpanAttributes.HTTP_HOST, @@ -38,14 +49,14 @@ ] _client_duration_attrs_new = [ - _SPAN_ATTRIBUTES_ERROR_TYPE, - SpanAttributes.HTTP_REQUEST_METHOD, - SpanAttributes.HTTP_RESPONSE_STATUS_CODE, - SpanAttributes.NETWORK_PROTOCOL_VERSION, - SpanAttributes.SERVER_ADDRESS, - SpanAttributes.SERVER_PORT, + ERROR_TYPE, + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_STATUS_CODE, + NETWORK_PROTOCOL_VERSION, + SERVER_ADDRESS, + SERVER_PORT, # TODO: Support opt-in for scheme in new semconv - # SpanAttributes.URL_SCHEME, + # URL_SCHEME, ] _server_duration_attrs_old = [ @@ -60,12 +71,12 @@ ] _server_duration_attrs_new = [ - _SPAN_ATTRIBUTES_ERROR_TYPE, - SpanAttributes.HTTP_REQUEST_METHOD, - SpanAttributes.HTTP_RESPONSE_STATUS_CODE, - SpanAttributes.HTTP_ROUTE, - SpanAttributes.NETWORK_PROTOCOL_VERSION, - SpanAttributes.URL_SCHEME, + ERROR_TYPE, + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_STATUS_CODE, + HTTP_ROUTE, + NETWORK_PROTOCOL_VERSION, + URL_SCHEME, ] _server_active_requests_count_attrs_old = [ @@ -79,8 +90,8 @@ ] _server_active_requests_count_attrs_new = [ - SpanAttributes.HTTP_REQUEST_METHOD, - SpanAttributes.URL_SCHEME, + HTTP_REQUEST_METHOD, + URL_SCHEME, ] OTEL_SEMCONV_STABILITY_OPT_IN = "OTEL_SEMCONV_STABILITY_OPT_IN" @@ -202,46 +213,40 @@ def _set_http_method(result, original, normalized, sem_conv_opt_in_mode): # See https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#common-attributes # Method is case sensitive. "http.request.method_original" should not be sanitized or automatically capitalized. if original != normalized and _report_new(sem_conv_opt_in_mode): - set_string_attribute( - result, SpanAttributes.HTTP_REQUEST_METHOD_ORIGINAL, original - ) + set_string_attribute(result, HTTP_REQUEST_METHOD_ORIGINAL, original) if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, SpanAttributes.HTTP_METHOD, normalized) if _report_new(sem_conv_opt_in_mode): - set_string_attribute( - result, SpanAttributes.HTTP_REQUEST_METHOD, normalized - ) + set_string_attribute(result, HTTP_REQUEST_METHOD, normalized) def _set_http_status_code(result, code, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): set_int_attribute(result, SpanAttributes.HTTP_STATUS_CODE, code) if _report_new(sem_conv_opt_in_mode): - set_int_attribute( - result, SpanAttributes.HTTP_RESPONSE_STATUS_CODE, code - ) + set_int_attribute(result, HTTP_RESPONSE_STATUS_CODE, code) def _set_http_url(result, url, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, SpanAttributes.HTTP_URL, url) if _report_new(sem_conv_opt_in_mode): - set_string_attribute(result, SpanAttributes.URL_FULL, url) + set_string_attribute(result, URL_FULL, url) def _set_http_scheme(result, scheme, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, SpanAttributes.HTTP_SCHEME, scheme) if _report_new(sem_conv_opt_in_mode): - set_string_attribute(result, SpanAttributes.URL_SCHEME, scheme) + set_string_attribute(result, URL_SCHEME, scheme) def _set_http_host(result, host, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, SpanAttributes.HTTP_HOST, host) if _report_new(sem_conv_opt_in_mode): - set_string_attribute(result, SpanAttributes.SERVER_ADDRESS, host) + set_string_attribute(result, SERVER_ADDRESS, host) # Client @@ -251,23 +256,21 @@ def _set_http_net_peer_name_client(result, peer_name, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, SpanAttributes.NET_PEER_NAME, peer_name) if _report_new(sem_conv_opt_in_mode): - set_string_attribute(result, SpanAttributes.SERVER_ADDRESS, peer_name) + set_string_attribute(result, SERVER_ADDRESS, peer_name) def _set_http_peer_port_client(result, port, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): set_int_attribute(result, SpanAttributes.NET_PEER_PORT, port) if _report_new(sem_conv_opt_in_mode): - set_int_attribute(result, SpanAttributes.SERVER_PORT, port) + set_int_attribute(result, SERVER_PORT, port) def _set_http_network_protocol_version(result, version, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, SpanAttributes.HTTP_FLAVOR, version) if _report_new(sem_conv_opt_in_mode): - set_string_attribute( - result, SpanAttributes.NETWORK_PROTOCOL_VERSION, version - ) + set_string_attribute(result, NETWORK_PROTOCOL_VERSION, version) # Server @@ -347,8 +350,8 @@ def _set_status( ): if status_code < 0: if _report_new(sem_conv_opt_in_mode): - span.set_attribute(_SPAN_ATTRIBUTES_ERROR_TYPE, status_code_str) - metrics_attributes[_SPAN_ATTRIBUTES_ERROR_TYPE] = status_code_str + span.set_attribute(ERROR_TYPE, status_code_str) + metrics_attributes[ERROR_TYPE] = status_code_str span.set_status( Status( @@ -370,12 +373,8 @@ def _set_status( status_code ) if status == StatusCode.ERROR: - span.set_attribute( - _SPAN_ATTRIBUTES_ERROR_TYPE, status_code_str - ) - metrics_attributes[_SPAN_ATTRIBUTES_ERROR_TYPE] = ( - status_code_str - ) + span.set_attribute(ERROR_TYPE, status_code_str) + metrics_attributes[ERROR_TYPE] = status_code_str span.set_status(Status(status)) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index ea8fa48046..3dfd97e0b2 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -65,7 +65,7 @@ "instrumentation": "opentelemetry-instrumentation-celery==0.47b0.dev", }, { - "library": "confluent-kafka >= 1.8.2, <= 2.3.0", + "library": "confluent-kafka >= 1.8.2, <= 2.4.0", "instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.47b0.dev", }, { diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py index 318aaeaa74..73c000ee9c 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py @@ -37,6 +37,10 @@ propagator = TraceContextTextMapPropagator() +_SUPPRESS_INSTRUMENTATION_KEY_PLAIN = ( + "suppress_instrumentation" # Set for backward compatibility +) + def extract_attributes_from_object( obj: any, attributes: Sequence[str], existing: Dict[str, str] = None @@ -161,9 +165,10 @@ def _python_path_without_directory(python_path, directory, path_separator): def is_instrumentation_enabled() -> bool: - if context.get_value(_SUPPRESS_INSTRUMENTATION_KEY): - return False - return True + return not ( + context.get_value(_SUPPRESS_INSTRUMENTATION_KEY) + or context.get_value(_SUPPRESS_INSTRUMENTATION_KEY_PLAIN) + ) def is_http_instrumentation_enabled() -> bool: @@ -188,7 +193,9 @@ def _suppress_instrumentation(*keys: str) -> Iterable[None]: @contextmanager def suppress_instrumentation() -> Iterable[None]: """Suppress instrumentation within the context.""" - with _suppress_instrumentation(_SUPPRESS_INSTRUMENTATION_KEY): + with _suppress_instrumentation( + _SUPPRESS_INSTRUMENTATION_KEY, _SUPPRESS_INSTRUMENTATION_KEY_PLAIN + ): yield diff --git a/opentelemetry-instrumentation/tests/test_utils.py b/opentelemetry-instrumentation/tests/test_utils.py index cf6cfdfd37..d3807a1bdb 100644 --- a/opentelemetry-instrumentation/tests/test_utils.py +++ b/opentelemetry-instrumentation/tests/test_utils.py @@ -15,10 +15,20 @@ import unittest from http import HTTPStatus +from opentelemetry.context import ( + _SUPPRESS_HTTP_INSTRUMENTATION_KEY, + _SUPPRESS_INSTRUMENTATION_KEY, + get_current, + get_value, +) from opentelemetry.instrumentation.sqlcommenter_utils import _add_sql_comment from opentelemetry.instrumentation.utils import ( _python_path_without_directory, http_status_to_status_code, + is_http_instrumentation_enabled, + is_instrumentation_enabled, + suppress_http_instrumentation, + suppress_instrumentation, ) from opentelemetry.trace import StatusCode @@ -186,3 +196,47 @@ def test_add_sql_comments_without_comments(self): ) self.assertEqual(commented_sql_without_semicolon, "Select 1") + + def test_is_instrumentation_enabled_by_default(self): + self.assertTrue(is_instrumentation_enabled()) + self.assertTrue(is_http_instrumentation_enabled()) + + def test_suppress_instrumentation(self): + with suppress_instrumentation(): + self.assertFalse(is_instrumentation_enabled()) + self.assertFalse(is_http_instrumentation_enabled()) + + self.assertTrue(is_instrumentation_enabled()) + self.assertTrue(is_http_instrumentation_enabled()) + + def test_suppress_http_instrumentation(self): + with suppress_http_instrumentation(): + self.assertFalse(is_http_instrumentation_enabled()) + self.assertTrue(is_instrumentation_enabled()) + + self.assertTrue(is_instrumentation_enabled()) + self.assertTrue(is_http_instrumentation_enabled()) + + def test_suppress_instrumentation_key(self): + self.assertIsNone(get_value(_SUPPRESS_INSTRUMENTATION_KEY)) + self.assertIsNone(get_value("suppress_instrumentation")) + + with suppress_instrumentation(): + ctx = get_current() + self.assertIn(_SUPPRESS_INSTRUMENTATION_KEY, ctx) + self.assertIn("suppress_instrumentation", ctx) + self.assertTrue(get_value(_SUPPRESS_INSTRUMENTATION_KEY)) + self.assertTrue(get_value("suppress_instrumentation")) + + self.assertIsNone(get_value(_SUPPRESS_INSTRUMENTATION_KEY)) + self.assertIsNone(get_value("suppress_instrumentation")) + + def test_suppress_http_instrumentation_key(self): + self.assertIsNone(get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY)) + + with suppress_http_instrumentation(): + ctx = get_current() + self.assertIn(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, ctx) + self.assertTrue(get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY)) + + self.assertIsNone(get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY)) diff --git a/propagator/opentelemetry-propagator-aws-xray/pyproject.toml b/propagator/opentelemetry-propagator-aws-xray/pyproject.toml index 69fd0bbbfa..4a3e22269a 100644 --- a/propagator/opentelemetry-propagator-aws-xray/pyproject.toml +++ b/propagator/opentelemetry-propagator-aws-xray/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ [project.entry-points.opentelemetry_propagator] xray = "opentelemetry.propagators.aws:AwsXRayPropagator" +xray_lambda = "opentelemetry.propagators.aws:AwsXRayLambdaPropagator" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/propagator/opentelemetry-propagator-aws-xray" diff --git a/propagator/opentelemetry-propagator-aws-xray/src/opentelemetry/propagators/aws/aws_xray_propagator.py b/propagator/opentelemetry-propagator-aws-xray/src/opentelemetry/propagators/aws/aws_xray_propagator.py index 4e4a6872ea..4966218211 100644 --- a/propagator/opentelemetry-propagator-aws-xray/src/opentelemetry/propagators/aws/aws_xray_propagator.py +++ b/propagator/opentelemetry-propagator-aws-xray/src/opentelemetry/propagators/aws/aws_xray_propagator.py @@ -58,6 +58,7 @@ import logging import typing +from os import environ from opentelemetry import trace from opentelemetry.context import Context @@ -71,6 +72,7 @@ ) TRACE_HEADER_KEY = "X-Amzn-Trace-Id" +AWS_TRACE_HEADER_ENV_KEY = "_X_AMZN_TRACE_ID" KV_PAIR_DELIMITER = ";" KEY_AND_VALUE_DELIMITER = "=" @@ -324,3 +326,33 @@ def fields(self): """Returns a set with the fields set in `inject`.""" return {TRACE_HEADER_KEY} + + +class AwsXrayLambdaPropagator(AwsXRayPropagator): + """Implementation of the AWS X-Ray Trace Header propagation protocol but + with special handling for Lambda's ``_X_AMZN_TRACE_ID` environment + variable. + """ + + def extract( + self, + carrier: CarrierT, + context: typing.Optional[Context] = None, + getter: Getter[CarrierT] = default_getter, + ) -> Context: + + xray_context = super().extract(carrier, context=context, getter=getter) + + if trace.get_current_span(context=context).get_span_context().is_valid: + return xray_context + + trace_header = environ.get(AWS_TRACE_HEADER_ENV_KEY) + + if trace_header is None: + return xray_context + + return super().extract( + {TRACE_HEADER_KEY: trace_header}, + context=xray_context, + getter=getter, + ) diff --git a/propagator/opentelemetry-propagator-aws-xray/test-requirements.txt b/propagator/opentelemetry-propagator-aws-xray/test-requirements.txt index 26f637bbb2..5e15e1c4a1 100644 --- a/propagator/opentelemetry-propagator-aws-xray/test-requirements.txt +++ b/propagator/opentelemetry-propagator-aws-xray/test-requirements.txt @@ -13,7 +13,7 @@ pytest-benchmark==4.0.0 requests==2.32.3 tomli==2.0.1 typing_extensions==4.10.0 -urllib3==2.2.1 +urllib3==2.2.2 wrapt==1.16.0 zipp==3.17.0 -e propagator/opentelemetry-propagator-aws-xray diff --git a/propagator/opentelemetry-propagator-aws-xray/tests/test_aws_xray_lambda_propagator.py b/propagator/opentelemetry-propagator-aws-xray/tests/test_aws_xray_lambda_propagator.py new file mode 100644 index 0000000000..a0432d1457 --- /dev/null +++ b/propagator/opentelemetry-propagator-aws-xray/tests/test_aws_xray_lambda_propagator.py @@ -0,0 +1,164 @@ +# 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 +from unittest import TestCase +from unittest.mock import patch + +from requests.structures import CaseInsensitiveDict + +from opentelemetry.context import get_current +from opentelemetry.propagators.aws.aws_xray_propagator import ( + TRACE_HEADER_KEY, + AwsXrayLambdaPropagator, +) +from opentelemetry.propagators.textmap import DefaultGetter +from opentelemetry.sdk.trace import ReadableSpan +from opentelemetry.trace import ( + Link, + NonRecordingSpan, + SpanContext, + TraceState, + get_current_span, + use_span, +) + + +class AwsXRayLambdaPropagatorTest(TestCase): + + def test_extract_no_environment_variable(self): + + actual_context = get_current_span( + AwsXrayLambdaPropagator().extract( + {}, context=get_current(), getter=DefaultGetter() + ) + ).get_span_context() + + self.assertEqual(hex(actual_context.trace_id), "0x0") + self.assertEqual(hex(actual_context.span_id), "0x0") + self.assertFalse( + actual_context.trace_flags.sampled, + ) + self.assertEqual(actual_context.trace_state, TraceState.get_default()) + + def test_extract_no_environment_variable_valid_context(self): + + with use_span(NonRecordingSpan(SpanContext(1, 2, False))): + + actual_context = get_current_span( + AwsXrayLambdaPropagator().extract( + {}, context=get_current(), getter=DefaultGetter() + ) + ).get_span_context() + + self.assertEqual(hex(actual_context.trace_id), "0x1") + self.assertEqual(hex(actual_context.span_id), "0x2") + self.assertFalse( + actual_context.trace_flags.sampled, + ) + self.assertEqual( + actual_context.trace_state, TraceState.get_default() + ) + + @patch.dict( + environ, + { + "_X_AMZN_TRACE_ID": ( + "Root=1-00000001-d188f8fa79d48a391a778fa6;" + "Parent=53995c3f42cd8ad8;Sampled=1;Foo=Bar" + ) + }, + ) + def test_extract_from_environment_variable(self): + + actual_context = get_current_span( + AwsXrayLambdaPropagator().extract( + {}, context=get_current(), getter=DefaultGetter() + ) + ).get_span_context() + + self.assertEqual( + hex(actual_context.trace_id), "0x1d188f8fa79d48a391a778fa6" + ) + self.assertEqual(hex(actual_context.span_id), "0x53995c3f42cd8ad8") + self.assertTrue( + actual_context.trace_flags.sampled, + ) + self.assertEqual(actual_context.trace_state, TraceState.get_default()) + + @patch.dict( + environ, + { + "_X_AMZN_TRACE_ID": ( + "Root=1-00000002-240000000000000000000002;" + "Parent=1600000000000002;Sampled=1;Foo=Bar" + ) + }, + ) + def test_add_link_from_environment_variable(self): + + propagator = AwsXrayLambdaPropagator() + + default_getter = DefaultGetter() + + carrier = CaseInsensitiveDict( + { + TRACE_HEADER_KEY: ( + "Root=1-00000001-240000000000000000000001;" + "Parent=1600000000000001;Sampled=1" + ) + } + ) + + extracted_context = propagator.extract( + carrier, context=get_current(), getter=default_getter + ) + + link_context = propagator.extract( + carrier, context=extracted_context, getter=default_getter + ) + + span = ReadableSpan( + "test", parent=extracted_context, links=[Link(link_context)] + ) + + span_parent_context = get_current_span(span.parent).get_span_context() + + self.assertEqual( + hex(span_parent_context.trace_id), "0x2240000000000000000000002" + ) + self.assertEqual( + hex(span_parent_context.span_id), "0x1600000000000002" + ) + self.assertTrue( + span_parent_context.trace_flags.sampled, + ) + self.assertEqual( + span_parent_context.trace_state, TraceState.get_default() + ) + + span_link_context = get_current_span( + span.links[0].context + ).get_span_context() + + self.assertEqual( + hex(span_link_context.trace_id), "0x1240000000000000000000001" + ) + self.assertEqual(hex(span_link_context.span_id), "0x1600000000000001") + self.assertTrue( + span_link_context.trace_flags.sampled, + ) + self.assertEqual( + span_link_context.trace_state, TraceState.get_default() + ) diff --git a/resource/opentelemetry-resource-detector-azure/CHANGELOG.md b/resource/opentelemetry-resource-detector-azure/CHANGELOG.md index f77fce18f1..5e16c83d63 100644 --- a/resource/opentelemetry-resource-detector-azure/CHANGELOG.md +++ b/resource/opentelemetry-resource-detector-azure/CHANGELOG.md @@ -5,6 +5,11 @@ 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 + +- Ensure consistently use of suppress_instrumentation utils + ([#2590](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2590)) + ## Version 0.1.5 (2024-05-16) - Ignore vm detector if already in other rps diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/vm.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/vm.py index 2112282949..63281a46e5 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/vm.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/vm.py @@ -17,12 +17,7 @@ from urllib.error import URLError from urllib.request import Request, urlopen -from opentelemetry.context import ( - _SUPPRESS_INSTRUMENTATION_KEY, - attach, - detach, - set_value, -) +from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.semconv.resource import ( CloudPlatformValues, @@ -46,15 +41,14 @@ class AzureVMResourceDetector(ResourceDetector): def detect(self) -> "Resource": attributes = {} if not _can_ignore_vm_detect(): - token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) - metadata_json = _get_azure_vm_metadata() - if not metadata_json: - return Resource(attributes) - for attribute_key in _EXPECTED_AZURE_AMS_ATTRIBUTES: - attributes[attribute_key] = _get_attribute_from_metadata( - metadata_json, attribute_key - ) - detach(token) + with suppress_instrumentation(): + metadata_json = _get_azure_vm_metadata() + if not metadata_json: + return Resource(attributes) + for attribute_key in _EXPECTED_AZURE_AMS_ATTRIBUTES: + attributes[attribute_key] = _get_attribute_from_metadata( + metadata_json, attribute_key + ) return Resource(attributes) diff --git a/tox.ini b/tox.ini index 03da302038..ec4902eafa 100644 --- a/tox.ini +++ b/tox.ini @@ -737,6 +737,11 @@ commands_pre = processor-baggage: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk processor-baggage: pip install -r {toxinidir}/processor/opentelemetry-processor-baggage/test-requirements.txt + http: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + http: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + http: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + http: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + http: pip install -r {toxinidir}/util/opentelemetry-util-http/test-requirements.txt http: pip install {toxinidir}/util/opentelemetry-util-http commands = @@ -1238,7 +1243,7 @@ deps = tomli==2.0.1 typing_extensions==4.10.0 tzdata==2024.1 - urllib3==1.26.18 + urllib3==1.26.19 vine==5.1.0 wcwidth==0.2.13 websocket-client==0.59.0 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 e8a2cf2034..f5dacf0fff 100644 --- a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py +++ b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py @@ -14,6 +14,7 @@ from __future__ import annotations +from collections.abc import Mapping from os import environ from re import IGNORECASE as RE_IGNORECASE from re import compile as re_compile @@ -87,32 +88,32 @@ def sanitize_header_value(self, header: str, value: str) -> str: def sanitize_header_values( self, - headers: dict[str, str], + headers: Mapping[str, str | list[str]], header_regexes: list[str], normalize_function: Callable[[str], str], - ) -> dict[str, str]: - values: dict[str, str] = {} + ) -> dict[str, list[str]]: + values: dict[str, list[str]] = {} if header_regexes: header_regexes_compiled = re_compile( - "|".join("^" + i + "$" for i in header_regexes), + "|".join(header_regexes), RE_IGNORECASE, ) - for header_name in list( - filter( - header_regexes_compiled.match, - headers.keys(), - ) - ): - header_values = headers.get(header_name) - if header_values: + for header_name, header_value in headers.items(): + if header_regexes_compiled.fullmatch(header_name): key = normalize_function(header_name.lower()) - values[key] = [ - self.sanitize_header_value( - header=header_name, value=header_values - ) - ] + if isinstance(header_value, str): + values[key] = [ + self.sanitize_header_value( + header_name, header_value + ) + ] + else: + values[key] = [ + self.sanitize_header_value(header_name, value) + for value in header_value + ] return values diff --git a/util/opentelemetry-util-http/test-requirements.txt b/util/opentelemetry-util-http/test-requirements.txt new file mode 100644 index 0000000000..0e28bbdd05 --- /dev/null +++ b/util/opentelemetry-util-http/test-requirements.txt @@ -0,0 +1,12 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +-e opentelemetry-instrumentation \ No newline at end of file diff --git a/util/opentelemetry-util-http/tests/test_sanitize_method.py b/util/opentelemetry-util-http/tests/test_sanitize_method.py index b4095324a6..bd14d88ee5 100644 --- a/util/opentelemetry-util-http/tests/test_sanitize_method.py +++ b/util/opentelemetry-util-http/tests/test_sanitize_method.py @@ -32,7 +32,7 @@ def test_standard_method_lowercase(self): def test_nonstandard_method(self): method = sanitize_method("UNKNOWN") - self.assertEqual(method, "NONSTANDARD") + self.assertEqual(method, "_OTHER") @patch.dict( "os.environ", @@ -42,4 +42,4 @@ def test_nonstandard_method(self): ) def test_nonstandard_method_allowed(self): method = sanitize_method("UNKNOWN") - self.assertEqual(method, "NONSTANDARD") + self.assertEqual(method, "UNKNOWN")