diff --git a/.github/workflows/instrumentations_0.yml b/.github/workflows/instrumentations_0.yml index fb8df0dcbb..d54cb50119 100644 --- a/.github/workflows/instrumentations_0.yml +++ b/.github/workflows/instrumentations_0.yml @@ -127,4 +127,4 @@ jobs: ~/.cache/pip key: v7-build-tox-cache-${{ env.RUN_MATRIX_COMBINATION }}-${{ hashFiles('tox.ini', 'gen-requirements.txt', 'dev-requirements.txt') }} - name: run tox - run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- -ra --benchmark-json=${{ env.RUN_MATRIX_COMBINATION }}-benchmark.json + run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- -ra diff --git a/.github/workflows/instrumentations_1.yml b/.github/workflows/instrumentations_1.yml index 7783105cd1..8c99eb0572 100644 --- a/.github/workflows/instrumentations_1.yml +++ b/.github/workflows/instrumentations_1.yml @@ -35,7 +35,9 @@ jobs: - "sdk-extension-aws" - "propagator-aws-xray" - "propagator-ot-trace" + - "resource-detector-azure" - "resource-detector-container" + - "util-http" os: [ubuntu-20.04] exclude: - python-version: pypy3 @@ -58,4 +60,4 @@ jobs: ~/.cache/pip key: v7-build-tox-cache-${{ env.RUN_MATRIX_COMBINATION }}-${{ hashFiles('tox.ini', 'gen-requirements.txt', 'dev-requirements.txt') }} - name: run tox - run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- -ra --benchmark-json=${{ env.RUN_MATRIX_COMBINATION }}-benchmark.json + run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- -ra diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index debecc6ff8..c156f7d942 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -68,8 +68,10 @@ jobs: - "processor-baggage" - "propagator-aws-xray" - "propagator-ot-trace" + - "resource-detector-azure" - "resource-detector-container" - "sdk-extension-aws" + - "util-http" os: [ubuntu-20.04] runs-on: ubuntu-20.04 steps: diff --git a/.gitignore b/.gitignore index c359e13727..1c32b4446a 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ _build/ # mypy .mypy_cache/ target + +# Benchmark result files +*-benchmark.json diff --git a/CHANGELOG.md b/CHANGELOG.md index b4da8ef772..ab5e36eb61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,6 @@ 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 @@ -20,13 +15,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#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-asyncpg` Add instrumentation to cursor based queries + ([#2501](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2501)) - `opentelemetry-instrumentation-confluent-kafka` Add support for produce purge ([#2638](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2638)) +- `opentelemetry-instrumentation-asgi` Implement new semantic convention opt-in with stable http semantic conventions + ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) +- `opentelemetry-instrumentation-httpx` Implement new semantic convention opt-in migration with stable http semantic conventions + ([#2631](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2631)) +- `opentelemetry-instrumentation-system-metrics` Permit to use psutil 6.0+. + ([#2630](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2630)) +- `opentelemetry-instrumentation-system-metrics` Add support for capture open file descriptors + ([#2652](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2652)) ### Breaking changes - `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-fastapi`, `opentelemetry-instrumentation-starlette` Use `tracer` and `meter` of originating components instead of one from `asgi` middleware ([#2580](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2580)) +- Populate `{method}` as `HTTP` on `_OTHER` methods from scope + ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) + ### Fixed @@ -38,6 +46,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#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)) +- `opentelemetry-instrumentation-psycopg` Bugfix: Handle empty statement. + ([#2644](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2644)) +- `opentelemetry-instrumentation-confluent-kafka` Confluent Kafka: Ensure consume span is ended when consumer is closed + ([#2640](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2640)) +- `opentelemetry-instrumentation-asgi` Fix generation of `http.target` and `http.url` attributes for ASGI apps + using sub apps + ([#2477](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2477)) +- `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-asyncio` instrumented `asyncio.wait_for` properly raises `asyncio.TimeoutError` as expected + ([#2637](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2637)) +- `opentelemetry-instrumentation-django` Handle exceptions from request/response hooks + ([#2153](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2153)) +- `opentelemetry-instrumentation-asgi` Removed `NET_HOST_NAME` AND `NET_HOST_PORT` from active requests count attribute + ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) + ## Version 1.25.0/0.46b0 (2024-05-31) @@ -145,7 +169,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2136](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2136)) - `opentelemetry-resource-detector-azure` Suppress instrumentation for `urllib` call ([#2178](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2178)) -- AwsLambdaInstrumentor handles and re-raises function exception ([#2245](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2245)) +- AwsLambdaInstrumentor handles and re-raises function exception + ([#2245](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2245)) ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f5a4913440..d7fc509c3a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,9 +96,7 @@ for more detail on available tox commands. ### Benchmarks -Performance progression of benchmarks for packages distributed by OpenTelemetry Python can be viewed as a [graph of throughput vs commit history](https://opentelemetry-python-contrib.readthedocs.io/en/latest/performance/benchmarks.html). From the linked page, you can download a JSON file with the performance results. - -Running the `tox` tests also runs the performance tests if any are available. Benchmarking tests are done with `pytest-benchmark` and they output a table with results to the console. +Some packages have benchmark tests. To run them, run `tox -f benchmark`. Benchmark tests use `pytest-benchmark` and they output a table with results to the console. To write benchmarks, simply use the [pytest benchmark fixture](https://pytest-benchmark.readthedocs.io/en/latest/usage.html#usage) like the following: @@ -114,10 +112,10 @@ def test_simple_start_span(benchmark): benchmark(benchmark_start_as_current_span, "benchmarkedSpan", 42) ``` -Make sure the test file is under the `tests/performance/benchmarks/` folder of +Make sure the test file is under the `benchmarks/` folder of the package it is benchmarking and further has a path that corresponds to the file in the package it is testing. Make sure that the file name begins with -`test_benchmark_`. (e.g. `propagator/opentelemetry-propagator-aws-xray/tests/performance/benchmarks/trace/propagation/test_benchmark_aws_xray_propagator.py`) +`test_benchmark_`. (e.g. `propagator/opentelemetry-propagator-aws-xray/benchmarks/trace/propagation/test_benchmark_aws_xray_propagator.py`) ## Pull Requests diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt b/exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt index b41245cd1f..8b47ad4093 100644 --- a/exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt @@ -11,7 +11,6 @@ pluggy==1.5.0 protobuf==4.25.3 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 python-snappy==0.7.1 requests==2.32.3 tomli==2.0.1 diff --git a/exporter/opentelemetry-exporter-richconsole/test-requirements.txt b/exporter/opentelemetry-exporter-richconsole/test-requirements.txt index af50fa87e8..eaa7ac3afa 100644 --- a/exporter/opentelemetry-exporter-richconsole/test-requirements.txt +++ b/exporter/opentelemetry-exporter-richconsole/test-requirements.txt @@ -10,7 +10,6 @@ pluggy==1.5.0 py-cpuinfo==9.0.0 Pygments==2.17.2 pytest==7.4.4 -pytest-benchmark==4.0.0 rich==13.7.1 tomli==2.0.1 typing_extensions==4.10.0 diff --git a/instrumentation/README.md b/instrumentation/README.md index 682334db1a..eb21717843 100644 --- a/instrumentation/README.md +++ b/instrumentation/README.md @@ -5,7 +5,7 @@ | [opentelemetry-instrumentation-aiohttp-client](./opentelemetry-instrumentation-aiohttp-client) | aiohttp ~= 3.0 | No | experimental | [opentelemetry-instrumentation-aiohttp-server](./opentelemetry-instrumentation-aiohttp-server) | aiohttp ~= 3.0 | No | experimental | [opentelemetry-instrumentation-aiopg](./opentelemetry-instrumentation-aiopg) | aiopg >= 0.13.0, < 2.0.0 | No | experimental -| [opentelemetry-instrumentation-asgi](./opentelemetry-instrumentation-asgi) | asgiref ~= 3.0 | No | experimental +| [opentelemetry-instrumentation-asgi](./opentelemetry-instrumentation-asgi) | asgiref ~= 3.0 | Yes | migration | [opentelemetry-instrumentation-asyncio](./opentelemetry-instrumentation-asyncio) | asyncio | No | experimental | [opentelemetry-instrumentation-asyncpg](./opentelemetry-instrumentation-asyncpg) | asyncpg >= 0.12.0 | No | experimental | [opentelemetry-instrumentation-aws-lambda](./opentelemetry-instrumentation-aws-lambda) | aws_lambda | No | experimental @@ -22,7 +22,7 @@ | [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | experimental | [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration | [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio ~= 1.27 | No | experimental -| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | No | experimental +| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | No | migration | [opentelemetry-instrumentation-jinja2](./opentelemetry-instrumentation-jinja2) | jinja2 >= 2.7, < 4.0 | No | experimental | [opentelemetry-instrumentation-kafka-python](./opentelemetry-instrumentation-kafka-python) | kafka-python >= 2.0 | No | experimental | [opentelemetry-instrumentation-logging](./opentelemetry-instrumentation-logging) | logging | No | experimental diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-0.txt index 3744af85b1..d03f17a64a 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-0.txt +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-0.txt @@ -11,7 +11,6 @@ pamqp==3.1.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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-1.txt index bb35e2e2ba..09c96cdcfb 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-1.txt +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-1.txt @@ -11,7 +11,6 @@ pamqp==3.2.1 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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-2.txt index 3b17c80c25..691fcc27f4 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-2.txt +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-2.txt @@ -11,7 +11,6 @@ pamqp==3.2.1 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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-3.txt b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-3.txt index 4ea66f8dd6..f14b2e0d61 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-3.txt +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-3.txt @@ -11,7 +11,6 @@ pamqp==3.3.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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-aiohttp-client/test-requirements.txt index f597361598..3fff2f6f41 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/test-requirements.txt @@ -21,7 +21,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 requests==2.32.3 tomli==2.0.1 typing_extensions==4.10.0 diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt index fe7582a2bb..75d2363578 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt @@ -14,7 +14,6 @@ py-cpuinfo==9.0.0 pytest==7.4.4 pytest-aiohttp==1.0.5 pytest-asyncio==0.23.5 -pytest-benchmark==4.0.0 tomli==2.0.1 typing_extensions==4.10.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-aiopg/test-requirements.txt index 033cee4492..9474f62f43 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-aiopg/test-requirements.txt @@ -10,7 +10,6 @@ pluggy==1.5.0 psycopg2-binary==2.9.9 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 tomli==2.0.1 typing_extensions==4.10.0 wrapt==1.16.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 f24160fcb6..f006f9b0c9 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -201,6 +201,31 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A from asgiref.compatibility import guarantee_single_callable from opentelemetry import context, trace +from opentelemetry.instrumentation._semconv import ( + _filter_semconv_active_request_count_attr, + _filter_semconv_duration_attrs, + _get_schema_url, + _HTTPStabilityMode, + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _report_new, + _report_old, + _server_active_requests_count_attrs_new, + _server_active_requests_count_attrs_old, + _server_duration_attrs_new, + _server_duration_attrs_old, + _set_http_flavor_version, + _set_http_host, + _set_http_method, + _set_http_net_host_port, + _set_http_peer_ip, + _set_http_peer_port_server, + _set_http_scheme, + _set_http_target, + _set_http_url, + _set_http_user_agent, + _set_status, +) from opentelemetry.instrumentation.asgi.types import ( ClientRequestHook, ClientResponseHook, @@ -210,27 +235,31 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A from opentelemetry.instrumentation.propagators import ( get_global_response_propagator, ) -from opentelemetry.instrumentation.utils import ( - _start_internal_or_server_span, - http_status_to_status_code, -) +from opentelemetry.instrumentation.utils import _start_internal_or_server_span from opentelemetry.metrics import get_meter from opentelemetry.propagators.textmap import Getter, Setter +from opentelemetry.semconv._incubating.metrics.http_metrics import ( + create_http_server_active_requests, + create_http_server_request_body_size, + create_http_server_response_body_size, +) 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 import set_span_in_context -from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, SanitizeValue, - _parse_active_request_count_attrs, - _parse_duration_attrs, + _parse_url_query, get_custom_headers, normalise_request_header_name, normalise_response_header_name, remove_url_credentials, + sanitize_method, ) @@ -295,7 +324,10 @@ def set( asgi_setter = ASGISetter() -def collect_request_attributes(scope): +# pylint: disable=too-many-branches +def collect_request_attributes( + scope, sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT +): """Collects HTTP request attributes from the ASGI scope and returns a dictionary to be used as span creation attributes.""" server_host, port, http_url = get_host_port_url_tuple(scope) @@ -304,31 +336,52 @@ def collect_request_attributes(scope): if isinstance(query_string, bytes): query_string = query_string.decode("utf8") http_url += "?" + urllib.parse.unquote(query_string) + result = {} + + scheme = scope.get("scheme") + if scheme: + _set_http_scheme(result, scheme, sem_conv_opt_in_mode) + if server_host: + _set_http_host(result, server_host, sem_conv_opt_in_mode) + if port: + _set_http_net_host_port(result, port, sem_conv_opt_in_mode) + flavor = scope.get("http_version") + if flavor: + _set_http_flavor_version(result, flavor, sem_conv_opt_in_mode) + path = scope.get("path") + if path: + _set_http_target( + result, path, path, query_string, sem_conv_opt_in_mode + ) + if http_url: + _set_http_url( + result, remove_url_credentials(http_url), sem_conv_opt_in_mode + ) - result = { - SpanAttributes.HTTP_SCHEME: scope.get("scheme"), - SpanAttributes.HTTP_HOST: server_host, - SpanAttributes.NET_HOST_PORT: port, - SpanAttributes.HTTP_FLAVOR: scope.get("http_version"), - SpanAttributes.HTTP_TARGET: scope.get("path"), - SpanAttributes.HTTP_URL: remove_url_credentials(http_url), - } - http_method = scope.get("method") + http_method = scope.get("method", "") if http_method: - result[SpanAttributes.HTTP_METHOD] = http_method + _set_http_method( + result, + http_method, + sanitize_method(http_method), + sem_conv_opt_in_mode, + ) http_host_value_list = asgi_getter.get(scope, "host") if http_host_value_list: - result[SpanAttributes.HTTP_SERVER_NAME] = ",".join( - http_host_value_list - ) + if _report_old(sem_conv_opt_in_mode): + result[SpanAttributes.HTTP_SERVER_NAME] = ",".join( + http_host_value_list + ) http_user_agent = asgi_getter.get(scope, "user-agent") if http_user_agent: - result[SpanAttributes.HTTP_USER_AGENT] = http_user_agent[0] + _set_http_user_agent(result, http_user_agent[0], sem_conv_opt_in_mode) if "client" in scope and scope["client"] is not None: - result[SpanAttributes.NET_PEER_IP] = scope.get("client")[0] - result[SpanAttributes.NET_PEER_PORT] = scope.get("client")[1] + _set_http_peer_ip(result, scope.get("client")[0], sem_conv_opt_in_mode) + _set_http_peer_port_server( + result, scope.get("client")[1], sem_conv_opt_in_mode + ) # remove None values result = {k: v for k, v in result.items() if v is not None} @@ -367,29 +420,41 @@ def get_host_port_url_tuple(scope): server = scope.get("server") or ["0.0.0.0", 80] port = server[1] server_host = server[0] + (":" + str(port) if str(port) != "80" else "") - full_path = scope.get("root_path", "") + scope.get("path", "") + # using the scope path is enough, see: + # - https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope (see: root_path and path) + # - https://asgi.readthedocs.io/en/latest/specs/www.html#wsgi-compatibility (see: PATH_INFO) + # PATH_INFO can be derived by stripping root_path from path + # -> that means that the path should contain the root_path already, so prefixing it again is not necessary + # - https://wsgi.readthedocs.io/en/latest/definitions.html#envvar-PATH_INFO + full_path = scope.get("path", "") http_url = scope.get("scheme", "http") + "://" + server_host + full_path return server_host, port, http_url -def set_status_code(span, status_code): +def set_status_code( + span, + status_code, + metric_attributes=None, + sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT, +): """Adds HTTP response attributes to span using the status_code argument.""" if not span.is_recording(): return + status_code_str = str(status_code) + try: status_code = int(status_code) except ValueError: - span.set_status( - Status( - StatusCode.ERROR, - "Non-integer HTTP status: " + repr(status_code), - ) - ) - else: - span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) - span.set_status( - Status(http_status_to_status_code(status_code, server_span=True)) - ) + status_code = -1 + if metric_attributes is None: + metric_attributes = {} + _set_status( + span, + metric_attributes, + status_code, + status_code_str, + sem_conv_opt_in_mode, + ) def get_default_span_details(scope: dict) -> Tuple[str, dict]: @@ -404,7 +469,9 @@ def get_default_span_details(scope: dict) -> Tuple[str, dict]: a tuple of the span name, and any attributes to attach to the span. """ path = scope.get("path", "").strip() - method = scope.get("method", "").strip() + method = sanitize_method(scope.get("method", "").strip()) + if method == "_OTHER": + method = "HTTP" if method and path: # http return f"{method} {path}", {} if path: # websocket @@ -478,13 +545,18 @@ def __init__( http_capture_headers_server_response: list[str] | None = None, http_capture_headers_sanitize_fields: list[str] | None = None, ): + # initialize semantic conventions opt-in if needed + _OpenTelemetrySemanticConventionStability._initialize() + sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP, + ) self.app = guarantee_single_callable(app) self.tracer = ( trace.get_tracer( __name__, __version__, tracer_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url(sem_conv_opt_in_mode), ) if tracer is None else tracer @@ -494,30 +566,51 @@ def __init__( __name__, __version__, meter_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url(sem_conv_opt_in_mode), ) if meter is None else meter ) - self.duration_histogram = self.meter.create_histogram( - name=MetricInstruments.HTTP_SERVER_DURATION, - unit="ms", - description="Duration of HTTP client requests.", - ) - self.server_response_size_histogram = self.meter.create_histogram( - name=MetricInstruments.HTTP_SERVER_RESPONSE_SIZE, - unit="By", - description="measures the size of HTTP response messages (compressed).", - ) - self.server_request_size_histogram = self.meter.create_histogram( - name=MetricInstruments.HTTP_SERVER_REQUEST_SIZE, - unit="By", - description="Measures the size of HTTP request messages (compressed).", - ) - self.active_requests_counter = self.meter.create_up_down_counter( - name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, - unit="requests", - description="measures the number of concurrent HTTP requests that are currently in-flight", + self.duration_histogram_old = None + if _report_old(sem_conv_opt_in_mode): + self.duration_histogram_old = self.meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_DURATION, + unit="ms", + description="Duration of HTTP server requests.", + ) + self.duration_histogram_new = None + if _report_new(sem_conv_opt_in_mode): + self.duration_histogram_new = self.meter.create_histogram( + name=HTTP_SERVER_REQUEST_DURATION, + description="Duration of HTTP server requests.", + unit="s", + ) + self.server_response_size_histogram = None + if _report_old(sem_conv_opt_in_mode): + self.server_response_size_histogram = self.meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_RESPONSE_SIZE, + unit="By", + description="measures the size of HTTP response messages (compressed).", + ) + self.server_response_body_size_histogram = None + if _report_new(sem_conv_opt_in_mode): + self.server_response_body_size_histogram = ( + create_http_server_response_body_size(self.meter) + ) + self.server_request_size_histogram = None + if _report_old(sem_conv_opt_in_mode): + self.server_request_size_histogram = self.meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_REQUEST_SIZE, + unit="By", + description="Measures the size of HTTP request messages (compressed).", + ) + self.server_request_body_size_histogram = None + if _report_new(sem_conv_opt_in_mode): + self.server_request_body_size_histogram = ( + create_http_server_request_body_size(self.meter) + ) + self.active_requests_counter = create_http_server_active_requests( + self.meter ) self.excluded_urls = excluded_urls self.default_span_details = ( @@ -527,6 +620,7 @@ def __init__( self.client_request_hook = client_request_hook self.client_response_hook = client_response_hook self.content_length_header = None + self._sem_conv_opt_in_mode = sem_conv_opt_in_mode # Environment variables as constructor parameters self.http_capture_headers_server_request = ( @@ -557,6 +651,7 @@ def __init__( or [] ) + # pylint: disable=too-many-statements async def __call__( self, scope: dict[str, Any], @@ -580,7 +675,9 @@ async def __call__( span_name, additional_attributes = self.default_span_details(scope) - attributes = collect_request_attributes(scope) + attributes = collect_request_attributes( + scope, self._sem_conv_opt_in_mode + ) attributes.update(additional_attributes) span, token = _start_internal_or_server_span( tracer=self.tracer, @@ -591,9 +688,9 @@ async def __call__( attributes=attributes, ) active_requests_count_attrs = _parse_active_request_count_attrs( - attributes + attributes, + self._sem_conv_opt_in_mode, ) - duration_attrs = _parse_duration_attrs(attributes) if scope["type"] == "http": self.active_requests_counter.add(1, active_requests_count_attrs) @@ -629,7 +726,7 @@ async def __call__( span_name, scope, send, - duration_attrs, + attributes, ) await self.app(scope, otel_receive, otel_send) @@ -637,16 +734,44 @@ async def __call__( if scope["type"] == "http": target = _collect_target_attribute(scope) if target: - duration_attrs[SpanAttributes.HTTP_TARGET] = target - duration = max(round((default_timer() - start) * 1000), 0) - self.duration_histogram.record(duration, duration_attrs) + path, query = _parse_url_query(target) + _set_http_target( + attributes, + target, + path, + query, + self._sem_conv_opt_in_mode, + ) + duration_s = default_timer() - start + duration_attrs_old = _parse_duration_attrs( + attributes, _HTTPStabilityMode.DEFAULT + ) + if target: + duration_attrs_old[SpanAttributes.HTTP_TARGET] = target + duration_attrs_new = _parse_duration_attrs( + attributes, _HTTPStabilityMode.HTTP + ) + if self.duration_histogram_old: + self.duration_histogram_old.record( + max(round(duration_s * 1000), 0), duration_attrs_old + ) + if self.duration_histogram_new: + self.duration_histogram_new.record( + max(duration_s, 0), duration_attrs_new + ) self.active_requests_counter.add( -1, active_requests_count_attrs ) if self.content_length_header: - self.server_response_size_histogram.record( - self.content_length_header, duration_attrs - ) + if self.server_response_size_histogram: + self.server_response_size_histogram.record( + self.content_length_header, duration_attrs_old + ) + if self.server_response_body_size_histogram: + self.server_response_body_size_histogram.record( + self.content_length_header, duration_attrs_new + ) + request_size = asgi_getter.get(scope, "content-length") if request_size: try: @@ -654,9 +779,14 @@ async def __call__( except ValueError: pass else: - self.server_request_size_histogram.record( - request_size_amount, duration_attrs - ) + if self.server_request_size_histogram: + self.server_request_size_histogram.record( + request_size_amount, duration_attrs_old + ) + if self.server_request_body_size_histogram: + self.server_request_body_size_histogram.record( + request_size_amount, duration_attrs_new + ) if token: context.detach(token) if span.is_recording(): @@ -675,7 +805,12 @@ async def otel_receive(): self.client_request_hook(receive_span, scope, message) if receive_span.is_recording(): if message["type"] == "websocket.receive": - set_status_code(receive_span, 200) + set_status_code( + receive_span, + 200, + None, + self._sem_conv_opt_in_mode, + ) receive_span.set_attribute( "asgi.event.type", message["type"] ) @@ -684,7 +819,12 @@ async def otel_receive(): return otel_receive def _get_otel_send( - self, server_span, server_span_name, scope, send, duration_attrs + self, + server_span, + server_span_name, + scope, + send, + duration_attrs, ): expecting_trailers = False @@ -699,16 +839,33 @@ async def otel_send(message: dict[str, Any]): if send_span.is_recording(): if message["type"] == "http.response.start": status_code = message["status"] - duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = ( - status_code + # We record metrics only once + set_status_code( + server_span, + status_code, + duration_attrs, + self._sem_conv_opt_in_mode, + ) + set_status_code( + send_span, + status_code, + None, + self._sem_conv_opt_in_mode, ) - set_status_code(server_span, status_code) - set_status_code(send_span, status_code) - expecting_trailers = message.get("trailers", False) elif message["type"] == "websocket.send": - set_status_code(server_span, 200) - set_status_code(send_span, 200) + set_status_code( + server_span, + 200, + duration_attrs, + self._sem_conv_opt_in_mode, + ) + set_status_code( + send_span, + 200, + None, + self._sem_conv_opt_in_mode, + ) send_span.set_attribute("asgi.event.type", message["type"]) if ( server_span.is_recording() @@ -761,3 +918,25 @@ async def otel_send(message: dict[str, Any]): server_span.end() return otel_send + + +def _parse_duration_attrs( + req_attrs, sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT +): + return _filter_semconv_duration_attrs( + req_attrs, + _server_duration_attrs_old, + _server_duration_attrs_new, + sem_conv_opt_in_mode, + ) + + +def _parse_active_request_count_attrs( + req_attrs, sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT +): + return _filter_semconv_active_request_count_attr( + req_attrs, + _server_active_requests_count_attrs_old, + _server_active_requests_count_attrs_new, + sem_conv_opt_in_mode, + ) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/package.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/package.py index c219ec5499..cd35b1f73a 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/package.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/package.py @@ -14,3 +14,7 @@ _instruments = ("asgiref ~= 3.0",) + +_supports_metrics = True + +_semconv_status = "migration" diff --git a/instrumentation/opentelemetry-instrumentation-asgi/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-asgi/test-requirements.txt index 9411be3b90..5313cff55f 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-asgi/test-requirements.txt @@ -6,7 +6,6 @@ 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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py index d2fe6bc52b..ff266cb5bf 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py @@ -23,6 +23,15 @@ import opentelemetry.instrumentation.asgi as otel_asgi from opentelemetry import trace as trace_api +from opentelemetry.instrumentation._semconv import ( + OTEL_SEMCONV_STABILITY_OPT_IN, + _HTTPStabilityMode, + _OpenTelemetrySemanticConventionStability, + _server_active_requests_count_attrs_new, + _server_active_requests_count_attrs_old, + _server_duration_attrs_new, + _server_duration_attrs_old, +) from opentelemetry.instrumentation.propagators import ( TraceResponsePropagator, get_global_response_propagator, @@ -33,6 +42,30 @@ HistogramDataPoint, NumberDataPoint, ) +from opentelemetry.semconv.attributes.client_attributes import ( + CLIENT_ADDRESS, + CLIENT_PORT, +) +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.attributes.user_agent_attributes import ( + USER_AGENT_ORIGINAL, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.asgitestutil import ( AsgiTestBase, @@ -40,24 +73,42 @@ ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind, format_span_id, format_trace_id -from opentelemetry.util.http import ( - _active_requests_count_attrs, - _duration_attrs, -) -_expected_metric_names = [ +_expected_metric_names_old = [ "http.server.active_requests", "http.server.duration", "http.server.response.size", "http.server.request.size", ] -_recommended_attrs = { - "http.server.active_requests": _active_requests_count_attrs, - "http.server.duration": _duration_attrs, - "http.server.response.size": _duration_attrs, - "http.server.request.size": _duration_attrs, +_expected_metric_names_new = [ + "http.server.active_requests", + "http.server.request.duration", + "http.server.response.body.size", + "http.server.request.body.size", +] +_expected_metric_names_both = _expected_metric_names_old +_expected_metric_names_both.extend(_expected_metric_names_new) + +_recommended_attrs_old = { + "http.server.active_requests": _server_active_requests_count_attrs_old, + "http.server.duration": _server_duration_attrs_old, + "http.server.response.size": _server_duration_attrs_old, + "http.server.request.size": _server_duration_attrs_old, +} + +_recommended_attrs_new = { + "http.server.active_requests": _server_active_requests_count_attrs_new, + "http.server.request.duration": _server_duration_attrs_new, + "http.server.response.body.size": _server_duration_attrs_new, + "http.server.request.body.size": _server_duration_attrs_new, } +_recommended_attrs_both = _recommended_attrs_old.copy() +_recommended_attrs_both.update(_recommended_attrs_new) +_recommended_attrs_both["http.server.active_requests"].extend( + _server_active_requests_count_attrs_old +) + _SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S = 0.01 @@ -229,7 +280,37 @@ async def error_asgi(scope, receive, send): # pylint: disable=too-many-public-methods class TestAsgiApplication(AsgiTestBase): - def validate_outputs(self, outputs, error=None, modifiers=None): + def setUp(self): + super().setUp() + + test_name = "" + if hasattr(self, "_testMethodName"): + test_name = self._testMethodName + sem_conv_mode = "default" + if "new_semconv" in test_name: + sem_conv_mode = "http" + elif "both_semconv" in test_name: + sem_conv_mode = "http/dup" + self.env_patch = mock.patch.dict( + "os.environ", + { + OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, + }, + ) + + _OpenTelemetrySemanticConventionStability._initialized = False + + self.env_patch.start() + + # pylint: disable=too-many-locals + def validate_outputs( + self, + outputs, + error=None, + modifiers=None, + old_sem_conv=True, + new_sem_conv=False, + ): # Ensure modifiers is a list modifiers = modifiers or [] # Check for expected outputs @@ -264,7 +345,7 @@ def validate_outputs(self, outputs, error=None, modifiers=None): # Check spans span_list = self.memory_exporter.get_finished_spans() - expected = [ + expected_old = [ { "name": "GET / http receive", "kind": trace_api.SpanKind.INTERNAL, @@ -300,6 +381,96 @@ def validate_outputs(self, outputs, error=None, modifiers=None): }, }, ] + expected_new = [ + { + "name": "GET / http receive", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": {"asgi.event.type": "http.request"}, + }, + { + "name": "GET / http send", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": { + HTTP_RESPONSE_STATUS_CODE: 200, + "asgi.event.type": "http.response.start", + }, + }, + { + "name": "GET / http send", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": {"asgi.event.type": "http.response.body"}, + }, + { + "name": "GET /", + "kind": trace_api.SpanKind.SERVER, + "attributes": { + HTTP_REQUEST_METHOD: "GET", + URL_SCHEME: "http", + SERVER_PORT: 80, + SERVER_ADDRESS: "127.0.0.1", + NETWORK_PROTOCOL_VERSION: "1.0", + URL_PATH: "/", + URL_FULL: "http://127.0.0.1/", + CLIENT_ADDRESS: "127.0.0.1", + CLIENT_PORT: 32767, + HTTP_RESPONSE_STATUS_CODE: 200, + }, + }, + ] + expected_both = [ + { + "name": "GET / http receive", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": {"asgi.event.type": "http.request"}, + }, + { + "name": "GET / http send", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": { + SpanAttributes.HTTP_STATUS_CODE: 200, + HTTP_RESPONSE_STATUS_CODE: 200, + "asgi.event.type": "http.response.start", + }, + }, + { + "name": "GET / http send", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": {"asgi.event.type": "http.response.body"}, + }, + { + "name": "GET /", + "kind": trace_api.SpanKind.SERVER, + "attributes": { + HTTP_REQUEST_METHOD: "GET", + URL_SCHEME: "http", + SERVER_PORT: 80, + SERVER_ADDRESS: "127.0.0.1", + NETWORK_PROTOCOL_VERSION: "1.0", + URL_PATH: "/", + URL_FULL: "http://127.0.0.1/", + CLIENT_ADDRESS: "127.0.0.1", + CLIENT_PORT: 32767, + HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_SCHEME: "http", + SpanAttributes.NET_HOST_PORT: 80, + SpanAttributes.HTTP_HOST: "127.0.0.1", + SpanAttributes.HTTP_FLAVOR: "1.0", + SpanAttributes.HTTP_TARGET: "/", + SpanAttributes.HTTP_URL: "http://127.0.0.1/", + SpanAttributes.NET_PEER_IP: "127.0.0.1", + SpanAttributes.NET_PEER_PORT: 32767, + SpanAttributes.HTTP_STATUS_CODE: 200, + }, + }, + ] + expected = expected_old + if new_sem_conv: + if old_sem_conv: + expected = expected_both + else: + expected = expected_new + # Run our expected modifiers for modifier in modifiers: expected = modifier(expected) @@ -322,6 +493,22 @@ def test_basic_asgi_call(self): outputs = self.get_all_output() self.validate_outputs(outputs) + def test_basic_asgi_call_new_semconv(self): + """Test that spans are emitted as expected.""" + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + outputs = self.get_all_output() + self.validate_outputs(outputs, old_sem_conv=False, new_sem_conv=True) + + def test_basic_asgi_call_both_semconv(self): + """Test that spans are emitted as expected.""" + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + outputs = self.get_all_output() + self.validate_outputs(outputs, old_sem_conv=True, new_sem_conv=True) + def test_asgi_not_recording(self): mock_tracer = mock.Mock() mock_span = mock.Mock() @@ -496,6 +683,59 @@ def update_expected_server(expected): outputs = self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_server]) + def test_behavior_with_scope_server_as_none_new_semconv(self): + """Test that middleware is ok when server is none in scope.""" + + def update_expected_server(expected): + expected[3]["attributes"].update( + { + SERVER_ADDRESS: "0.0.0.0", + SERVER_PORT: 80, + URL_FULL: "http://0.0.0.0/", + } + ) + return expected + + self.scope["server"] = None + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + outputs = self.get_all_output() + self.validate_outputs( + outputs, + modifiers=[update_expected_server], + old_sem_conv=False, + new_sem_conv=True, + ) + + def test_behavior_with_scope_server_as_none_both_semconv(self): + """Test that middleware is ok when server is none in scope.""" + + def update_expected_server(expected): + expected[3]["attributes"].update( + { + SpanAttributes.HTTP_HOST: "0.0.0.0", + SpanAttributes.NET_HOST_PORT: 80, + SpanAttributes.HTTP_URL: "http://0.0.0.0/", + SERVER_ADDRESS: "0.0.0.0", + SERVER_PORT: 80, + URL_FULL: "http://0.0.0.0/", + } + ) + return expected + + self.scope["server"] = None + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + outputs = self.get_all_output() + self.validate_outputs( + outputs, + modifiers=[update_expected_server], + old_sem_conv=True, + new_sem_conv=True, + ) + def test_host_header(self): """Test that host header is converted to http.server_name.""" hostname = b"server_name_1" @@ -513,6 +753,28 @@ def update_expected_server(expected): outputs = self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_server]) + def test_host_header_both_semconv(self): + """Test that host header is converted to http.server_name.""" + hostname = b"server_name_1" + + def update_expected_server(expected): + expected[3]["attributes"].update( + {SpanAttributes.HTTP_SERVER_NAME: hostname.decode("utf8")} + ) + return expected + + self.scope["headers"].append([b"host", hostname]) + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + outputs = self.get_all_output() + self.validate_outputs( + outputs, + modifiers=[update_expected_server], + old_sem_conv=True, + new_sem_conv=True, + ) + def test_user_agent(self): """Test that host header is converted to http.server_name.""" user_agent = b"test-agent" @@ -530,6 +792,53 @@ def update_expected_user_agent(expected): outputs = self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_user_agent]) + def test_user_agent_new_semconv(self): + """Test that host header is converted to http.server_name.""" + user_agent = b"test-agent" + + def update_expected_user_agent(expected): + expected[3]["attributes"].update( + {USER_AGENT_ORIGINAL: user_agent.decode("utf8")} + ) + return expected + + self.scope["headers"].append([b"user-agent", user_agent]) + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + outputs = self.get_all_output() + self.validate_outputs( + outputs, + modifiers=[update_expected_user_agent], + old_sem_conv=False, + new_sem_conv=True, + ) + + def test_user_agent_both_semconv(self): + """Test that host header is converted to http.server_name.""" + user_agent = b"test-agent" + + def update_expected_user_agent(expected): + expected[3]["attributes"].update( + { + SpanAttributes.HTTP_USER_AGENT: user_agent.decode("utf8"), + USER_AGENT_ORIGINAL: user_agent.decode("utf8"), + } + ) + return expected + + self.scope["headers"].append([b"user-agent", user_agent]) + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + outputs = self.get_all_output() + self.validate_outputs( + outputs, + modifiers=[update_expected_user_agent], + old_sem_conv=True, + new_sem_conv=True, + ) + def test_traceresponse_header(self): """Test a traceresponse header is sent when a global propagator is set.""" @@ -565,6 +874,7 @@ def test_traceresponse_header(self): def test_websocket(self): self.scope = { + "method": "GET", "type": "websocket", "http_version": "1.1", "scheme": "ws", @@ -584,17 +894,17 @@ def test_websocket(self): self.assertEqual(len(span_list), 6) expected = [ { - "name": "/ websocket receive", + "name": "GET / websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.connect"}, }, { - "name": "/ websocket send", + "name": "GET / websocket send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.accept"}, }, { - "name": "/ websocket receive", + "name": "GET / websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.receive", @@ -602,7 +912,7 @@ def test_websocket(self): }, }, { - "name": "/ websocket send", + "name": "GET / websocket send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.send", @@ -610,12 +920,12 @@ def test_websocket(self): }, }, { - "name": "/ websocket receive", + "name": "GET / websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.disconnect"}, }, { - "name": "/", + "name": "GET /", "kind": trace_api.SpanKind.SERVER, "attributes": { SpanAttributes.HTTP_SCHEME: self.scope["scheme"], @@ -627,6 +937,167 @@ def test_websocket(self): SpanAttributes.NET_PEER_IP: self.scope["client"][0], SpanAttributes.NET_PEER_PORT: self.scope["client"][1], SpanAttributes.HTTP_STATUS_CODE: 200, + SpanAttributes.HTTP_METHOD: self.scope["method"], + }, + }, + ] + for span, expected in zip(span_list, expected): + self.assertEqual(span.name, expected["name"]) + self.assertEqual(span.kind, expected["kind"]) + self.assertDictEqual(dict(span.attributes), expected["attributes"]) + + def test_websocket_new_semconv(self): + self.scope = { + "method": "GET", + "type": "websocket", + "http_version": "1.1", + "scheme": "ws", + "path": "/", + "query_string": b"", + "headers": [], + "client": ("127.0.0.1", 32767), + "server": ("127.0.0.1", 80), + } + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_input({"type": "websocket.connect"}) + self.send_input({"type": "websocket.receive", "text": "ping"}) + self.send_input({"type": "websocket.disconnect"}) + self.get_all_output() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 6) + expected = [ + { + "name": "GET / websocket receive", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": {"asgi.event.type": "websocket.connect"}, + }, + { + "name": "GET / websocket send", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": {"asgi.event.type": "websocket.accept"}, + }, + { + "name": "GET / websocket receive", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": { + "asgi.event.type": "websocket.receive", + HTTP_RESPONSE_STATUS_CODE: 200, + }, + }, + { + "name": "GET / websocket send", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": { + "asgi.event.type": "websocket.send", + HTTP_RESPONSE_STATUS_CODE: 200, + }, + }, + { + "name": "GET / websocket receive", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": {"asgi.event.type": "websocket.disconnect"}, + }, + { + "name": "GET /", + "kind": trace_api.SpanKind.SERVER, + "attributes": { + URL_SCHEME: self.scope["scheme"], + SERVER_PORT: self.scope["server"][1], + SERVER_ADDRESS: self.scope["server"][0], + NETWORK_PROTOCOL_VERSION: self.scope["http_version"], + URL_PATH: self.scope["path"], + URL_FULL: f'{self.scope["scheme"]}://{self.scope["server"][0]}{self.scope["path"]}', + CLIENT_ADDRESS: self.scope["client"][0], + CLIENT_PORT: self.scope["client"][1], + HTTP_RESPONSE_STATUS_CODE: 200, + HTTP_REQUEST_METHOD: self.scope["method"], + }, + }, + ] + for span, expected in zip(span_list, expected): + self.assertEqual(span.name, expected["name"]) + self.assertEqual(span.kind, expected["kind"]) + self.assertDictEqual(dict(span.attributes), expected["attributes"]) + + def test_websocket_both_semconv(self): + self.scope = { + "method": "GET", + "type": "websocket", + "http_version": "1.1", + "scheme": "ws", + "path": "/", + "query_string": b"", + "headers": [], + "client": ("127.0.0.1", 32767), + "server": ("127.0.0.1", 80), + } + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_input({"type": "websocket.connect"}) + self.send_input({"type": "websocket.receive", "text": "ping"}) + self.send_input({"type": "websocket.disconnect"}) + self.get_all_output() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 6) + expected = [ + { + "name": "GET / websocket receive", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": {"asgi.event.type": "websocket.connect"}, + }, + { + "name": "GET / websocket send", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": {"asgi.event.type": "websocket.accept"}, + }, + { + "name": "GET / websocket receive", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": { + "asgi.event.type": "websocket.receive", + HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.HTTP_STATUS_CODE: 200, + }, + }, + { + "name": "GET / websocket send", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": { + "asgi.event.type": "websocket.send", + HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.HTTP_STATUS_CODE: 200, + }, + }, + { + "name": "GET / websocket receive", + "kind": trace_api.SpanKind.INTERNAL, + "attributes": {"asgi.event.type": "websocket.disconnect"}, + }, + { + "name": "GET /", + "kind": trace_api.SpanKind.SERVER, + "attributes": { + SpanAttributes.HTTP_SCHEME: self.scope["scheme"], + SpanAttributes.NET_HOST_PORT: self.scope["server"][1], + SpanAttributes.HTTP_HOST: self.scope["server"][0], + SpanAttributes.HTTP_FLAVOR: self.scope["http_version"], + SpanAttributes.HTTP_TARGET: self.scope["path"], + SpanAttributes.HTTP_URL: f'{self.scope["scheme"]}://{self.scope["server"][0]}{self.scope["path"]}', + SpanAttributes.NET_PEER_IP: self.scope["client"][0], + SpanAttributes.NET_PEER_PORT: self.scope["client"][1], + SpanAttributes.HTTP_STATUS_CODE: 200, + SpanAttributes.HTTP_METHOD: self.scope["method"], + URL_SCHEME: self.scope["scheme"], + SERVER_PORT: self.scope["server"][1], + SERVER_ADDRESS: self.scope["server"][0], + NETWORK_PROTOCOL_VERSION: self.scope["http_version"], + URL_PATH: self.scope["path"], + URL_FULL: f'{self.scope["scheme"]}://{self.scope["server"][0]}{self.scope["path"]}', + CLIENT_ADDRESS: self.scope["client"][0], + CLIENT_PORT: self.scope["client"][1], + HTTP_RESPONSE_STATUS_CODE: 200, + HTTP_REQUEST_METHOD: self.scope["method"], }, }, ] @@ -737,7 +1208,7 @@ def test_asgi_metrics(self): "opentelemetry.instrumentation.asgi", ) for metric in scope_metric.metrics: - self.assertIn(metric.name, _expected_metric_names) + self.assertIn(metric.name, _expected_metric_names_old) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: @@ -748,7 +1219,79 @@ def test_asgi_metrics(self): number_data_point_seen = True for attr in point.attributes: self.assertIn( - attr, _recommended_attrs[metric.name] + attr, _recommended_attrs_old[metric.name] + ) + self.assertTrue(number_data_point_seen and histogram_data_point_seen) + + def test_asgi_metrics_new_semconv(self): + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + self.seed_app(app) + self.send_default_request() + self.seed_app(app) + self.send_default_request() + metrics_list = self.memory_metrics_reader.get_metrics_data() + number_data_point_seen = False + histogram_data_point_seen = False + self.assertTrue(len(metrics_list.resource_metrics) != 0) + for resource_metric in metrics_list.resource_metrics: + self.assertTrue(len(resource_metric.scope_metrics) != 0) + for scope_metric in resource_metric.scope_metrics: + self.assertTrue(len(scope_metric.metrics) != 0) + self.assertEqual( + scope_metric.scope.name, + "opentelemetry.instrumentation.asgi", + ) + for metric in scope_metric.metrics: + self.assertIn(metric.name, _expected_metric_names_new) + data_points = list(metric.data.data_points) + self.assertEqual(len(data_points), 1) + for point in data_points: + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 3) + histogram_data_point_seen = True + if isinstance(point, NumberDataPoint): + number_data_point_seen = True + for attr in point.attributes: + self.assertIn( + attr, _recommended_attrs_new[metric.name] + ) + self.assertTrue(number_data_point_seen and histogram_data_point_seen) + + def test_asgi_metrics_both_semconv(self): + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + self.seed_app(app) + self.send_default_request() + self.seed_app(app) + self.send_default_request() + metrics_list = self.memory_metrics_reader.get_metrics_data() + number_data_point_seen = False + histogram_data_point_seen = False + self.assertTrue(len(metrics_list.resource_metrics) != 0) + for resource_metric in metrics_list.resource_metrics: + self.assertTrue(len(resource_metric.scope_metrics) != 0) + for scope_metric in resource_metric.scope_metrics: + self.assertTrue(len(scope_metric.metrics) != 0) + self.assertEqual( + scope_metric.scope.name, + "opentelemetry.instrumentation.asgi", + ) + for metric in scope_metric.metrics: + self.assertIn(metric.name, _expected_metric_names_both) + data_points = list(metric.data.data_points) + self.assertEqual(len(data_points), 1) + for point in data_points: + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 3) + histogram_data_point_seen = True + if isinstance(point, NumberDataPoint): + number_data_point_seen = True + for attr in point.attributes: + self.assertIn( + attr, _recommended_attrs_both[metric.name] ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) @@ -799,6 +1342,141 @@ def test_basic_metric_success(self): ) self.assertEqual(point.value, 0) + def test_basic_metric_success_new_semconv(self): + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + start = default_timer() + self.send_default_request() + duration_s = max(default_timer() - start, 0) + expected_duration_attributes = { + "http.request.method": "GET", + "url.scheme": "http", + "network.protocol.version": "1.0", + "http.response.status_code": 200, + } + expected_requests_count_attributes = { + "http.request.method": "GET", + "url.scheme": "http", + } + metrics_list = self.memory_metrics_reader.get_metrics_data() + # pylint: disable=too-many-nested-blocks + for resource_metric in metrics_list.resource_metrics: + for scope_metrics in resource_metric.scope_metrics: + for metric in scope_metrics.metrics: + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertDictEqual( + expected_duration_attributes, + dict(point.attributes), + ) + self.assertEqual(point.count, 1) + if metric.name == "http.server.request.duration": + self.assertAlmostEqual( + duration_s, point.sum, places=2 + ) + elif ( + metric.name == "http.server.response.body.size" + ): + self.assertEqual(1024, point.sum) + elif ( + metric.name == "http.server.request.body.size" + ): + self.assertEqual(128, point.sum) + elif isinstance(point, NumberDataPoint): + self.assertDictEqual( + expected_requests_count_attributes, + dict(point.attributes), + ) + self.assertEqual(point.value, 0) + + def test_basic_metric_success_both_semconv(self): + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + start = default_timer() + self.send_default_request() + duration = max(round((default_timer() - start) * 1000), 0) + duration_s = max(default_timer() - start, 0) + expected_duration_attributes_old = { + "http.method": "GET", + "http.host": "127.0.0.1", + "http.scheme": "http", + "http.flavor": "1.0", + "net.host.port": 80, + "http.status_code": 200, + } + expected_requests_count_attributes = { + "http.method": "GET", + "http.host": "127.0.0.1", + "http.scheme": "http", + "http.flavor": "1.0", + "http.request.method": "GET", + "url.scheme": "http", + } + expected_duration_attributes_new = { + "http.request.method": "GET", + "url.scheme": "http", + "network.protocol.version": "1.0", + "http.response.status_code": 200, + } + metrics_list = self.memory_metrics_reader.get_metrics_data() + # pylint: disable=too-many-nested-blocks + for resource_metric in metrics_list.resource_metrics: + for scope_metrics in resource_metric.scope_metrics: + for metric in scope_metrics.metrics: + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 1) + if metric.name == "http.server.request.duration": + self.assertAlmostEqual( + duration_s, point.sum, places=2 + ) + self.assertDictEqual( + expected_duration_attributes_new, + dict(point.attributes), + ) + elif ( + metric.name == "http.server.response.body.size" + ): + self.assertEqual(1024, point.sum) + self.assertDictEqual( + expected_duration_attributes_new, + dict(point.attributes), + ) + elif ( + metric.name == "http.server.request.body.size" + ): + self.assertEqual(128, point.sum) + self.assertDictEqual( + expected_duration_attributes_new, + dict(point.attributes), + ) + elif metric.name == "http.server.duration": + self.assertAlmostEqual( + duration, point.sum, delta=5 + ) + self.assertDictEqual( + expected_duration_attributes_old, + dict(point.attributes), + ) + elif metric.name == "http.server.response.size": + self.assertEqual(1024, point.sum) + self.assertDictEqual( + expected_duration_attributes_old, + dict(point.attributes), + ) + elif metric.name == "http.server.request.size": + self.assertEqual(128, point.sum) + self.assertDictEqual( + expected_duration_attributes_old, + dict(point.attributes), + ) + elif isinstance(point, NumberDataPoint): + self.assertDictEqual( + expected_requests_count_attributes, + dict(point.attributes), + ) + self.assertEqual(point.value, 0) + def test_metric_target_attribute(self): expected_target = "/api/user/{id}" @@ -882,6 +1560,70 @@ def test_request_attributes(self): }, ) + def test_request_attributes_new_semconv(self): + self.scope["query_string"] = b"foo=bar" + headers = [] + headers.append((b"host", b"test")) + self.scope["headers"] = headers + + attrs = otel_asgi.collect_request_attributes( + self.scope, + _HTTPStabilityMode.HTTP, + ) + + self.assertDictEqual( + attrs, + { + HTTP_REQUEST_METHOD: "GET", + SERVER_ADDRESS: "127.0.0.1", + URL_PATH: "/", + URL_QUERY: "foo=bar", + URL_FULL: "http://127.0.0.1/?foo=bar", + SERVER_PORT: 80, + URL_SCHEME: "http", + NETWORK_PROTOCOL_VERSION: "1.0", + CLIENT_ADDRESS: "127.0.0.1", + CLIENT_PORT: 32767, + }, + ) + + def test_request_attributes_both_semconv(self): + self.scope["query_string"] = b"foo=bar" + headers = [] + headers.append((b"host", b"test")) + self.scope["headers"] = headers + + attrs = otel_asgi.collect_request_attributes( + self.scope, + _HTTPStabilityMode.HTTP_DUP, + ) + + self.assertDictEqual( + attrs, + { + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_HOST: "127.0.0.1", + SpanAttributes.HTTP_TARGET: "/", + SpanAttributes.HTTP_URL: "http://127.0.0.1/?foo=bar", + SpanAttributes.NET_HOST_PORT: 80, + SpanAttributes.HTTP_SCHEME: "http", + SpanAttributes.HTTP_SERVER_NAME: "test", + SpanAttributes.HTTP_FLAVOR: "1.0", + SpanAttributes.NET_PEER_IP: "127.0.0.1", + SpanAttributes.NET_PEER_PORT: 32767, + HTTP_REQUEST_METHOD: "GET", + SERVER_ADDRESS: "127.0.0.1", + URL_PATH: "/", + URL_QUERY: "foo=bar", + URL_FULL: "http://127.0.0.1/?foo=bar", + SERVER_PORT: 80, + URL_SCHEME: "http", + NETWORK_PROTOCOL_VERSION: "1.0", + CLIENT_ADDRESS: "127.0.0.1", + CLIENT_PORT: 32767, + }, + ) + def test_query_string(self): self.scope["query_string"] = b"foo=bar" attrs = otel_asgi.collect_request_attributes(self.scope) @@ -889,6 +1631,25 @@ def test_query_string(self): attrs[SpanAttributes.HTTP_URL], "http://127.0.0.1/?foo=bar" ) + def test_query_string_new_semconv(self): + self.scope["query_string"] = b"foo=bar" + attrs = otel_asgi.collect_request_attributes( + self.scope, + _HTTPStabilityMode.HTTP, + ) + self.assertEqual(attrs[URL_FULL], "http://127.0.0.1/?foo=bar") + + def test_query_string_both_semconv(self): + self.scope["query_string"] = b"foo=bar" + attrs = otel_asgi.collect_request_attributes( + self.scope, + _HTTPStabilityMode.HTTP_DUP, + ) + self.assertEqual(attrs[URL_FULL], "http://127.0.0.1/?foo=bar") + self.assertEqual( + attrs[SpanAttributes.HTTP_URL], "http://127.0.0.1/?foo=bar" + ) + def test_query_string_percent_bytes(self): self.scope["query_string"] = b"foo%3Dbar" attrs = otel_asgi.collect_request_attributes(self.scope) @@ -910,6 +1671,32 @@ def test_response_attributes(self): self.assertEqual(self.span.set_attribute.call_count, 1) self.span.set_attribute.assert_has_calls(expected, any_order=True) + def test_response_attributes_new_semconv(self): + otel_asgi.set_status_code( + self.span, + 404, + None, + _HTTPStabilityMode.HTTP, + ) + expected = (mock.call(HTTP_RESPONSE_STATUS_CODE, 404),) + self.assertEqual(self.span.set_attribute.call_count, 1) + self.assertEqual(self.span.set_attribute.call_count, 1) + self.span.set_attribute.assert_has_calls(expected, any_order=True) + + def test_response_attributes_both_semconv(self): + otel_asgi.set_status_code( + self.span, + 404, + None, + _HTTPStabilityMode.HTTP_DUP, + ) + expected = (mock.call(SpanAttributes.HTTP_STATUS_CODE, 404),) + expected2 = (mock.call(HTTP_RESPONSE_STATUS_CODE, 404),) + self.assertEqual(self.span.set_attribute.call_count, 2) + self.assertEqual(self.span.set_attribute.call_count, 2) + self.span.set_attribute.assert_has_calls(expected, any_order=True) + self.span.set_attribute.assert_has_calls(expected2, any_order=True) + def test_response_attributes_invalid_status_code(self): otel_asgi.set_status_code(self.span, "Invalid Status Code") self.assertEqual(self.span.set_status.call_count, 1) diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py index fc1b535270..f5f0d34c4f 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py @@ -280,8 +280,11 @@ async def trace_coroutine(self, coro): # CancelledError is raised when a coroutine is cancelled # before it has a chance to run. We don't want to record # this as an error. + # Still it needs to be raised in order for `asyncio.wait_for` + # to properly work with timeout and raise accordingly `asyncio.TimeoutError` except asyncio.CancelledError: attr["state"] = "cancelled" + raise except Exception as exc: exception = exc state = determine_state(exception) diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-asyncio/test-requirements.txt index 4943fcc851..d9be806a58 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncio/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-asyncio/test-requirements.txt @@ -7,7 +7,6 @@ pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 pytest-asyncio==0.23.5 -pytest-benchmark==4.0.0 tomli==2.0.1 typing_extensions==4.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_wait.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_wait.py index 77064aeafa..312f035d36 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_wait.py +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_wait.py @@ -68,6 +68,19 @@ async def main(): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) + def test_asyncio_wait_for_with_timeout(self): + expected_timeout_error = None + + async def main(): + nonlocal expected_timeout_error + try: + await asyncio.wait_for(async_func(), 0.01) + except asyncio.TimeoutError as timeout_error: + expected_timeout_error = timeout_error + + asyncio.run(main()) + self.assertNotEqual(expected_timeout_error, None) + def test_asyncio_as_completed(self): async def main(): if sys.version_info >= (3, 11): diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py index 798a5dc00b..ba76254aa8 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py @@ -127,15 +127,27 @@ def _instrument(self, **kwargs): "asyncpg.connection", method, self._do_execute ) - def _uninstrument(self, **__): for method in [ - "execute", - "executemany", - "fetch", - "fetchval", - "fetchrow", + "Cursor.fetch", + "Cursor.forward", + "Cursor.fetchrow", + "CursorIterator.__anext__", ]: - unwrap(asyncpg.Connection, method) + wrapt.wrap_function_wrapper( + "asyncpg.cursor", method, self._do_cursor_execute + ) + + def _uninstrument(self, **__): + for cls, methods in [ + ( + asyncpg.connection.Connection, + ("execute", "executemany", "fetch", "fetchval", "fetchrow"), + ), + (asyncpg.cursor.Cursor, ("forward", "fetch", "fetchrow")), + (asyncpg.cursor.CursorIterator, ("__anext__",)), + ]: + for method_name in methods: + unwrap(cls, method_name) async def _do_execute(self, func, instance, args, kwargs): exception = None @@ -170,3 +182,49 @@ async def _do_execute(self, func, instance, args, kwargs): span.set_status(Status(StatusCode.ERROR)) return result + + async def _do_cursor_execute(self, func, instance, args, kwargs): + """Wrap cursor based functions. For every call this will generate a new span.""" + exception = None + params = getattr(instance._connection, "_params", {}) + name = ( + instance._query + if instance._query + else params.get("database", "postgresql") + ) + + try: + # Strip leading comments so we get the operation name. + name = self._leading_comment_remover.sub("", name).split()[0] + except IndexError: + name = "" + + stop = False + with self._tracer.start_as_current_span( + f"CURSOR: {name}", + kind=SpanKind.CLIENT, + ) as span: + if span.is_recording(): + span_attributes = _hydrate_span_from_args( + instance._connection, + instance._query, + instance._args if self.capture_parameters else None, + ) + for attribute, value in span_attributes.items(): + span.set_attribute(attribute, value) + + try: + result = await func(*args, **kwargs) + except StopAsyncIteration: + # Do not show this exception to the span + stop = True + except Exception as exc: # pylint: disable=W0703 + exception = exc + raise + finally: + if span.is_recording() and exception is not None: + span.set_status(Status(StatusCode.ERROR)) + + if not stop: + return result + raise StopAsyncIteration diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-asyncpg/test-requirements.txt index 2ef86b3d94..de992b55b2 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/test-requirements.txt @@ -8,7 +8,6 @@ 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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py b/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py index 12aad0c6dc..7c88b9c005 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py @@ -1,4 +1,9 @@ -from asyncpg import Connection +import asyncio +from unittest import mock + +import pytest +from asyncpg import Connection, Record, cursor +from wrapt import ObjectProxy from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor from opentelemetry.test.test_base import TestBase @@ -34,3 +39,69 @@ def test_duplicated_uninstrumentation(self): self.assertFalse( hasattr(method, "_opentelemetry_ext_asyncpg_applied") ) + + def test_cursor_instrumentation(self): + def assert_wrapped(assert_fnc): + for cls, methods in [ + (cursor.Cursor, ("forward", "fetch", "fetchrow")), + (cursor.CursorIterator, ("__anext__",)), + ]: + for method_name in methods: + method = getattr(cls, method_name, None) + assert_fnc( + isinstance(method, ObjectProxy), + f"{method} isinstance {type(method)}", + ) + + assert_wrapped(self.assertFalse) + AsyncPGInstrumentor().instrument() + assert_wrapped(self.assertTrue) + AsyncPGInstrumentor().uninstrument() + assert_wrapped(self.assertFalse) + + def test_cursor_span_creation(self): + """Test the cursor wrapper if it creates spans correctly.""" + + # Mock out all interaction with postgres + async def bind_mock(*args, **kwargs): + return [] + + async def exec_mock(*args, **kwargs): + return [], None, True + + conn = mock.Mock() + conn.is_closed = lambda: False + + conn._protocol = mock.Mock() + conn._protocol.bind = bind_mock + conn._protocol.execute = exec_mock + conn._protocol.bind_execute = exec_mock + conn._protocol.close_portal = bind_mock + + state = mock.Mock() + state.closed = False + + apg = AsyncPGInstrumentor() + apg.instrument(tracer_provider=self.tracer_provider) + + # init the cursor and fetch a single record + crs = cursor.Cursor(conn, "SELECT * FROM test", state, [], Record) + asyncio.run(crs._init(1)) + asyncio.run(crs.fetch(1)) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].name, "CURSOR: SELECT") + self.assertTrue(spans[0].status.is_ok) + + # Now test that the StopAsyncIteration of the cursor does not get recorded as an ERROR + crs_iter = cursor.CursorIterator( + conn, "SELECT * FROM test", state, [], Record, 1, 1 + ) + + with pytest.raises(StopAsyncIteration): + asyncio.run(crs_iter.__anext__()) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + self.assertEqual([span.status.is_ok for span in spans], [True, True]) diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-aws-lambda/test-requirements.txt index b5168dc7fe..515b1929d1 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/test-requirements.txt @@ -6,7 +6,6 @@ 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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-boto/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-boto/test-requirements.txt index 54c8bb0558..fb49507cc2 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-boto/test-requirements.txt @@ -20,7 +20,6 @@ pluggy==1.5.0 py-cpuinfo==9.0.0 pycparser==2.21 pytest==7.4.4 -pytest-benchmark==4.0.0 python-dateutil==2.8.2 pytz==2024.1 PyYAML==6.0.1 diff --git a/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt index 54fcf790d7..2105a36c3c 100644 --- a/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt @@ -9,7 +9,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 python-dateutil==2.8.2 s3transfer==0.10.0 six==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt index f9fe9abe8a..c9f23944a9 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt @@ -20,7 +20,6 @@ pluggy==1.5.0 py-cpuinfo==9.0.0 pycparser==2.21 pytest==7.4.4 -pytest-benchmark==4.0.0 python-dateutil==2.8.2 pytz==2024.1 PyYAML==6.0.1 diff --git a/instrumentation/opentelemetry-instrumentation-cassandra/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-cassandra/test-requirements.txt index f0d811982b..6004a5ee89 100644 --- a/instrumentation/opentelemetry-instrumentation-cassandra/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-cassandra/test-requirements.txt @@ -9,7 +9,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 PyYAML==6.0.1 scylla-driver==3.26.6 six==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-celery/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-celery/test-requirements-0.txt index 3159fefaf8..1000a2e8b7 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/test-requirements-0.txt +++ b/instrumentation/opentelemetry-instrumentation-celery/test-requirements-0.txt @@ -16,7 +16,6 @@ pluggy==1.5.0 prompt-toolkit==3.0.43 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 python-dateutil==2.8.2 six==1.16.0 tomli==2.0.1 diff --git a/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt index 6012f89379..7bed7ab671 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt +++ b/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt @@ -15,7 +15,6 @@ pluggy==1.5.0 prompt-toolkit==3.0.43 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 python-dateutil==2.8.2 six==1.16.0 tomli==2.0.1 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 45d45ccb63..3d1cc79c93 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 @@ -144,6 +144,10 @@ def consume( ): # pylint: disable=useless-super-delegation return super().consume(*args, **kwargs) + # This method is deliberately implemented in order to allow wrapt to wrap this function + def close(self): # pylint: disable=useless-super-delegation + return super().close() + class ProxiedProducer(Producer): def __init__(self, producer: Producer, tracer: Tracer): @@ -181,6 +185,11 @@ def __init__(self, consumer: Consumer, tracer: Tracer): self._current_consume_span = None self._current_context_token = None + def close(self): + return ConfluentKafkaInstrumentor.wrap_close( + self._consumer.close, self + ) + def committed(self, partitions, timeout=-1): return self._consumer.committed(partitions, timeout) @@ -303,6 +312,9 @@ def _inner_wrap_consume(func, instance, args, kwargs): func, instance, self._tracer, args, kwargs ) + def _inner_wrap_close(func, instance): + return ConfluentKafkaInstrumentor.wrap_close(func, instance) + wrapt.wrap_function_wrapper( AutoInstrumentedProducer, "produce", @@ -321,6 +333,12 @@ def _inner_wrap_consume(func, instance, args, kwargs): _inner_wrap_consume, ) + wrapt.wrap_function_wrapper( + AutoInstrumentedConsumer, + "close", + _inner_wrap_close, + ) + def _uninstrument(self, **kwargs): confluent_kafka.Producer = self._original_kafka_producer confluent_kafka.Consumer = self._original_kafka_consumer @@ -403,3 +421,9 @@ def wrap_consume(func, instance, tracer, args, kwargs): ) return records + + @staticmethod + def wrap_close(func, instance): + if instance._current_consume_span: + _end_current_consume_span(instance) + func() diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-confluent-kafka/test-requirements.txt index 87387ded81..0f114fb9cd 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/test-requirements.txt @@ -7,7 +7,6 @@ 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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests/test_instrumentation.py b/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests/test_instrumentation.py index 205de27733..27653d6777 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests/test_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests/test_instrumentation.py @@ -237,7 +237,44 @@ def test_consume(self) -> None: span_list = self.memory_exporter.get_finished_spans() self._compare_spans(span_list, expected_spans) + def test_close(self) -> None: + instrumentation = ConfluentKafkaInstrumentor() + mocked_messages = [ + MockedMessage("topic-a", 0, 0, []), + ] + expected_spans = [ + {"name": "recv", "attributes": {}}, + { + "name": "topic-a process", + "attributes": { + SpanAttributes.MESSAGING_OPERATION: "process", + SpanAttributes.MESSAGING_KAFKA_PARTITION: 0, + SpanAttributes.MESSAGING_SYSTEM: "kafka", + SpanAttributes.MESSAGING_DESTINATION: "topic-a", + SpanAttributes.MESSAGING_DESTINATION_KIND: MessagingDestinationKindValues.QUEUE.value, + SpanAttributes.MESSAGING_MESSAGE_ID: "topic-a.0.0", + }, + }, + ] + + consumer = MockConsumer( + mocked_messages, + { + "bootstrap.servers": "localhost:29092", + "group.id": "mygroup", + "auto.offset.reset": "earliest", + }, + ) + self.memory_exporter.clear() + consumer = instrumentation.instrument_consumer(consumer) + consumer.poll() + consumer.close() + + span_list = self.memory_exporter.get_finished_spans() + self._compare_spans(span_list, expected_spans) + def _compare_spans(self, spans, expected_spans): + self.assertEqual(len(spans), len(expected_spans)) for span, expected_span in zip(spans, expected_spans): self.assertEqual(expected_span["name"], span.name) for attribute_key, expected_attribute_value in expected_span[ diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-dbapi/test-requirements.txt index d35a55f831..30df307b5c 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-dbapi/test-requirements.txt @@ -6,7 +6,6 @@ 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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py index 1b747fd2c0..6b64865ef7 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py @@ -187,6 +187,7 @@ def _get_span_name(request): return request.method # pylint: disable=too-many-locals + # pylint: disable=too-many-branches def process_request(self, request): # request.META is a dictionary containing all available HTTP headers # Read more about request.META here: @@ -286,9 +287,14 @@ def process_request(self, request): request.META[self._environ_token] = token if _DjangoMiddleware._otel_request_hook: - _DjangoMiddleware._otel_request_hook( # pylint: disable=not-callable - span, request - ) + try: + _DjangoMiddleware._otel_request_hook( # pylint: disable=not-callable + span, request + ) + except Exception: # pylint: disable=broad-exception-caught + # Raising an exception here would leak the request span since process_response + # would not be called. Log the exception instead. + _logger.exception("Exception raised by request_hook") # pylint: disable=unused-argument def process_view(self, request, view_func, *args, **kwargs): @@ -385,10 +391,14 @@ def process_response(self, request, response): # record any exceptions raised while processing the request exception = request.META.pop(self._environ_exception_key, None) + if _DjangoMiddleware._otel_response_hook: - _DjangoMiddleware._otel_response_hook( # pylint: disable=not-callable - span, request, response - ) + try: + _DjangoMiddleware._otel_response_hook( # pylint: disable=not-callable + span, request, response + ) + except Exception: # pylint: disable=broad-exception-caught + _logger.exception("Exception raised by response_hook") if exception: activation.__exit__( diff --git a/instrumentation/opentelemetry-instrumentation-django/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-django/test-requirements-0.txt index a6162e7c00..5231354b50 100644 --- a/instrumentation/opentelemetry-instrumentation-django/test-requirements-0.txt +++ b/instrumentation/opentelemetry-instrumentation-django/test-requirements-0.txt @@ -7,7 +7,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 pytz==2024.1 sqlparse==0.5.0 tomli==2.0.1 diff --git a/instrumentation/opentelemetry-instrumentation-django/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-django/test-requirements-1.txt index 58f5d5c88b..a2c4e1faf2 100644 --- a/instrumentation/opentelemetry-instrumentation-django/test-requirements-1.txt +++ b/instrumentation/opentelemetry-instrumentation-django/test-requirements-1.txt @@ -7,7 +7,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 pytz==2024.1 sqlparse==0.5.0 tomli==2.0.1 diff --git a/instrumentation/opentelemetry-instrumentation-django/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-django/test-requirements-2.txt index ffc43f4023..5794660465 100644 --- a/instrumentation/opentelemetry-instrumentation-django/test-requirements-2.txt +++ b/instrumentation/opentelemetry-instrumentation-django/test-requirements-2.txt @@ -8,7 +8,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 sqlparse==0.5.0 tomli==2.0.1 typing_extensions==4.10.0 diff --git a/instrumentation/opentelemetry-instrumentation-django/test-requirements-3.txt b/instrumentation/opentelemetry-instrumentation-django/test-requirements-3.txt index 92ebaa83e4..cf75a2cfc2 100644 --- a/instrumentation/opentelemetry-instrumentation-django/test-requirements-3.txt +++ b/instrumentation/opentelemetry-instrumentation-django/test-requirements-3.txt @@ -7,7 +7,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 sqlparse==0.5.0 tomli==2.0.1 typing_extensions==4.10.0 diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 63af1e6b86..c6b0568ef8 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -392,6 +392,32 @@ def response_hook(span, request, response): self.assertIsInstance(response_hook_args[2], HttpResponse) self.assertEqual(response_hook_args[2], response) + def test_request_hook_exception(self): + def request_hook(span, request): + # pylint: disable=broad-exception-raised + raise Exception("request hook exception") + + _DjangoMiddleware._otel_request_hook = request_hook + Client().get("/span_name/1234/") + _DjangoMiddleware._otel_request_hook = None + + # ensure that span ended + finished_spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(finished_spans), 1) + + def test_response_hook_exception(self): + def response_hook(span, request, response): + # pylint: disable=broad-exception-raised + raise Exception("response hook exception") + + _DjangoMiddleware._otel_response_hook = response_hook + Client().get("/span_name/1234/") + _DjangoMiddleware._otel_response_hook = None + + # ensure that span ended + finished_spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(finished_spans), 1) + def test_trace_parent(self): id_generator = RandomIdGenerator() trace_id = format_trace_id(id_generator.generate_trace_id()) diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-0.txt index e6d9bb6f9d..1f1f3057a1 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-0.txt +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-0.txt @@ -8,7 +8,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 python-dateutil==2.8.2 six==1.16.0 tomli==2.0.1 diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-1.txt index 12e3a1c229..60d7e24c76 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-1.txt +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-1.txt @@ -8,7 +8,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 python-dateutil==2.8.2 six==1.16.0 tomli==2.0.1 diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt index f34d67d9c8..bfcb79883f 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt @@ -9,7 +9,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 python-dateutil==2.8.2 six==1.16.0 tomli==2.0.1 diff --git a/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-0.txt index f17ada63f4..78db2d39ec 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-0.txt +++ b/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-0.txt @@ -7,7 +7,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 python-mimeparse==1.6.0 six==1.16.0 tomli==2.0.1 diff --git a/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-1.txt index 68b1aba13d..eb330346fb 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-1.txt +++ b/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-1.txt @@ -7,7 +7,6 @@ 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 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-2.txt index 4b4f8e7c0d..32ac062fbb 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-2.txt +++ b/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-2.txt @@ -7,7 +7,6 @@ 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 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py index bf7f1d4f49..dbc2512ca0 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py @@ -345,8 +345,6 @@ def test_falcon_metric_values(self): "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "falconframework.org", - "net.host.name": "falconframework.org", - "net.host.port": 80, } start = default_timer() self.client().simulate_get("/hello/756") diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.txt index 2116980b3f..8a77bc34d8 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.txt @@ -18,7 +18,6 @@ py-cpuinfo==9.0.0 pydantic==2.6.2 pydantic_core==2.16.3 pytest==7.4.4 -pytest-benchmark==4.0.0 requests==2.32.3 sniffio==1.3.0 starlette==0.36.3 diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 26d9e743a8..0ad63164d5 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -12,18 +12,14 @@ # 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 from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware -from fastapi.responses import JSONResponse from fastapi.testclient import TestClient import opentelemetry.instrumentation.fastapi as otel_fastapi -from opentelemetry import trace from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, @@ -31,12 +27,8 @@ ) from opentelemetry.sdk.resources import Resource from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.test.globals_test import reset_trace_globals from opentelemetry.test.test_base import TestBase from opentelemetry.util.http import ( - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, _active_requests_count_attrs, _duration_attrs, get_excluded_urls, @@ -62,7 +54,7 @@ } -class TestFastAPIManualInstrumentation(TestBase): +class TestBaseFastAPI(TestBase): def _create_app(self): app = self._create_fastapi_app() self._instrumentor.instrument_app( @@ -85,6 +77,15 @@ def _create_app_explicit_excluded_urls(self): ) return app + @classmethod + def setUpClass(cls): + if cls is TestBaseFastAPI: + raise unittest.SkipTest( + f"{cls.__name__} is an abstract base class" + ) + + super(TestBaseFastAPI, cls).setUpClass() + def setUp(self): super().setUp() self.env_patch = patch.dict( @@ -110,6 +111,155 @@ def tearDown(self): self._instrumentor.uninstrument() self._instrumentor.uninstrument_app(self._app) + @staticmethod + def _create_fastapi_app(): + app = fastapi.FastAPI() + sub_app = fastapi.FastAPI() + + @sub_app.get("/home") + async def _(): + return {"message": "sub hi"} + + @app.get("/foobar") + async def _(): + return {"message": "hello world"} + + @app.get("/user/{username}") + async def _(username: str): + return {"message": username} + + @app.get("/exclude/{param}") + async def _(param: str): + return {"message": param} + + @app.get("/healthzz") + async def _(): + return {"message": "ok"} + + app.mount("/sub", app=sub_app) + + return app + + +class TestBaseManualFastAPI(TestBaseFastAPI): + + @classmethod + def setUpClass(cls): + if cls is TestBaseManualFastAPI: + raise unittest.SkipTest( + f"{cls.__name__} is an abstract base class" + ) + + super(TestBaseManualFastAPI, cls).setUpClass() + + def test_sub_app_fastapi_call(self): + """ + This test is to ensure that a span in case of a sub app targeted contains the correct server url + + As this test case covers manual instrumentation, we won't see any additional spans for the sub app. + In this case all generated spans might suffice the requirements for the attributes already + (as the testcase is not setting a root_path for the outer app here) + """ + + self._client.get("/sub/home") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 3) + for span in spans: + # As we are only looking to the "outer" app, we would see only the "GET /sub" spans + self.assertIn("GET /sub", span.name) + + # We now want to specifically test all spans including the + # - HTTP_TARGET + # - HTTP_URL + # attributes to be populated with the expected values + spans_with_http_attributes = [ + span + for span in spans + if ( + SpanAttributes.HTTP_URL in span.attributes + or SpanAttributes.HTTP_TARGET in span.attributes + ) + ] + + # We expect only one span to have the HTTP attributes set (the SERVER span from the app itself) + # the sub app is not instrumented with manual instrumentation tests. + self.assertEqual(1, len(spans_with_http_attributes)) + + for span in spans_with_http_attributes: + self.assertEqual( + "/sub/home", span.attributes[SpanAttributes.HTTP_TARGET] + ) + self.assertEqual( + "https://testserver:443/sub/home", + span.attributes[SpanAttributes.HTTP_URL], + ) + + +class TestBaseAutoFastAPI(TestBaseFastAPI): + + @classmethod + def setUpClass(cls): + if cls is TestBaseAutoFastAPI: + raise unittest.SkipTest( + f"{cls.__name__} is an abstract base class" + ) + + super(TestBaseAutoFastAPI, cls).setUpClass() + + def test_sub_app_fastapi_call(self): + """ + This test is to ensure that a span in case of a sub app targeted contains the correct server url + + As this test case covers auto instrumentation, we will see additional spans for the sub app. + In this case all generated spans might suffice the requirements for the attributes already + (as the testcase is not setting a root_path for the outer app here) + """ + + self._client.get("/sub/home") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 6) + + for span in spans: + # As we are only looking to the "outer" app, we would see only the "GET /sub" spans + # -> the outer app is not aware of the sub_apps internal routes + sub_in = "GET /sub" in span.name + # The sub app spans are named GET /home as from the sub app perspective the request targets /home + # -> the sub app is technically not aware of the /sub prefix + home_in = "GET /home" in span.name + + # We expect the spans to be either from the outer app or the sub app + self.assertTrue( + sub_in or home_in, + f"Span {span.name} does not have /sub or /home in its name", + ) + + # We now want to specifically test all spans including the + # - HTTP_TARGET + # - HTTP_URL + # attributes to be populated with the expected values + spans_with_http_attributes = [ + span + for span in spans + if ( + SpanAttributes.HTTP_URL in span.attributes + or SpanAttributes.HTTP_TARGET in span.attributes + ) + ] + + # We now expect spans with attributes from both the app and its sub app + self.assertEqual(2, len(spans_with_http_attributes)) + + for span in spans_with_http_attributes: + self.assertEqual( + "/sub/home", span.attributes[SpanAttributes.HTTP_TARGET] + ) + self.assertEqual( + "https://testserver:443/sub/home", + span.attributes[SpanAttributes.HTTP_URL], + ) + + +class TestFastAPIManualInstrumentation(TestBaseManualFastAPI): def test_instrument_app_with_instrument(self): if not isinstance(self, TestAutoInstrumentation): self._instrumentor.instrument() @@ -322,6 +472,11 @@ def test_metric_uninstrument(self): @staticmethod def _create_fastapi_app(): app = fastapi.FastAPI() + sub_app = fastapi.FastAPI() + + @sub_app.get("/home") + async def _(): + return {"message": "sub hi"} @app.get("/foobar") async def _(): @@ -339,10 +494,12 @@ async def _(param: str): async def _(): return {"message": "ok"} + app.mount("/sub", app=sub_app) + return app -class TestFastAPIManualInstrumentationHooks(TestFastAPIManualInstrumentation): +class TestFastAPIManualInstrumentationHooks(TestBaseManualFastAPI): _server_request_hook = None _client_request_hook = None _client_response_hook = None @@ -392,7 +549,7 @@ def client_response_hook(send_span, scope, message): ) -class TestAutoInstrumentation(TestFastAPIManualInstrumentation): +class TestAutoInstrumentation(TestBaseAutoFastAPI): """Test the auto-instrumented variant Extending the manual instrumentation as most test cases apply @@ -453,8 +610,60 @@ def tearDown(self): self._instrumentor.uninstrument() super().tearDown() + def test_sub_app_fastapi_call(self): + """ + !!! Attention: we need to override this testcase for the auto-instrumented variant + The reason is, that with auto instrumentation, the sub app is instrumented as well + and therefore we would see the spans for the sub app as well + + This test is to ensure that a span in case of a sub app targeted contains the correct server url + """ + + self._client.get("/sub/home") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 6) + + for span in spans: + # As we are only looking to the "outer" app, we would see only the "GET /sub" spans + # -> the outer app is not aware of the sub_apps internal routes + sub_in = "GET /sub" in span.name + # The sub app spans are named GET /home as from the sub app perspective the request targets /home + # -> the sub app is technically not aware of the /sub prefix + home_in = "GET /home" in span.name + + # We expect the spans to be either from the outer app or the sub app + self.assertTrue( + sub_in or home_in, + f"Span {span.name} does not have /sub or /home in its name", + ) + + # We now want to specifically test all spans including the + # - HTTP_TARGET + # - HTTP_URL + # attributes to be populated with the expected values + spans_with_http_attributes = [ + span + for span in spans + if ( + SpanAttributes.HTTP_URL in span.attributes + or SpanAttributes.HTTP_TARGET in span.attributes + ) + ] + + # We now expect spans with attributes from both the app and its sub app + self.assertEqual(2, len(spans_with_http_attributes)) + + for span in spans_with_http_attributes: + self.assertEqual( + "/sub/home", span.attributes[SpanAttributes.HTTP_TARGET] + ) + self.assertEqual( + "https://testserver:443/sub/home", + span.attributes[SpanAttributes.HTTP_URL], + ) + -class TestAutoInstrumentationHooks(TestFastAPIManualInstrumentationHooks): +class TestAutoInstrumentationHooks(TestBaseAutoFastAPI): """ Test the auto-instrumented variant for request and response hooks @@ -494,6 +703,58 @@ def tearDown(self): self._instrumentor.uninstrument() super().tearDown() + def test_sub_app_fastapi_call(self): + """ + !!! Attention: we need to override this testcase for the auto-instrumented variant + The reason is, that with auto instrumentation, the sub app is instrumented as well + and therefore we would see the spans for the sub app as well + + This test is to ensure that a span in case of a sub app targeted contains the correct server url + """ + + self._client.get("/sub/home") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 6) + + for span in spans: + # As we are only looking to the "outer" app, we would see only the "GET /sub" spans + # -> the outer app is not aware of the sub_apps internal routes + sub_in = "GET /sub" in span.name + # The sub app spans are named GET /home as from the sub app perspective the request targets /home + # -> the sub app is technically not aware of the /sub prefix + home_in = "GET /home" in span.name + + # We expect the spans to be either from the outer app or the sub app + self.assertTrue( + sub_in or home_in, + f"Span {span.name} does not have /sub or /home in its name", + ) + + # We now want to specifically test all spans including the + # - HTTP_TARGET + # - HTTP_URL + # attributes to be populated with the expected values + spans_with_http_attributes = [ + span + for span in spans + if ( + SpanAttributes.HTTP_URL in span.attributes + or SpanAttributes.HTTP_TARGET in span.attributes + ) + ] + + # We now expect spans with attributes from both the app and its sub app + self.assertEqual(2, len(spans_with_http_attributes)) + + for span in spans_with_http_attributes: + self.assertEqual( + "/sub/home", span.attributes[SpanAttributes.HTTP_TARGET] + ) + self.assertEqual( + "https://testserver:443/sub/home", + span.attributes[SpanAttributes.HTTP_URL], + ) + class TestAutoInstrumentationLogic(unittest.TestCase): def test_instrumentation(self): @@ -511,412 +772,3 @@ def test_instrumentation(self): should_be_original = fastapi.FastAPI self.assertIs(original, should_be_original) - - -class TestWrappedApplication(TestBase): - def setUp(self): - super().setUp() - - self.app = fastapi.FastAPI() - - @self.app.get("/foobar") - async def _(): - return {"message": "hello world"} - - otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) - self.client = TestClient(self.app) - self.tracer = self.tracer_provider.get_tracer(__name__) - - def tearDown(self) -> None: - super().tearDown() - with self.disable_logging(): - otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) - - def test_mark_span_internal_in_presence_of_span_from_other_framework(self): - with self.tracer.start_as_current_span( - "test", kind=trace.SpanKind.SERVER - ) as parent_span: - resp = self.client.get("/foobar") - self.assertEqual(200, resp.status_code) - - span_list = self.memory_exporter.get_finished_spans() - for span in span_list: - print(str(span.__class__) + ": " + str(span.__dict__)) - - # there should be 4 spans - single SERVER "test" and three INTERNAL "FastAPI" - self.assertEqual(trace.SpanKind.INTERNAL, span_list[0].kind) - self.assertEqual(trace.SpanKind.INTERNAL, span_list[1].kind) - # main INTERNAL span - child of test - self.assertEqual(trace.SpanKind.INTERNAL, span_list[2].kind) - self.assertEqual( - parent_span.context.span_id, span_list[2].parent.span_id - ) - # SERVER "test" - self.assertEqual(trace.SpanKind.SERVER, span_list[3].kind) - self.assertEqual( - parent_span.context.span_id, span_list[3].context.span_id - ) - - -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", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", - }, -) -class TestHTTPAppWithCustomHeaders(TestBase): - def setUp(self): - super().setUp() - self.app = self._create_app() - otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) - self.client = TestClient(self.app) - - def tearDown(self) -> None: - super().tearDown() - with self.disable_logging(): - otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) - - @staticmethod - def _create_app(): - app = fastapi.FastAPI() - - @app.get("/foobar") - async def _(): - 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) - - return app - - def test_http_custom_request_headers_in_span_attributes(self): - expected = { - "http.request.header.custom_test_header_1": ( - "test-header-value-1", - ), - "http.request.header.custom_test_header_2": ( - "test-header-value-2", - ), - "http.request.header.regex_test_header_1": ("Regex Test Value 1",), - "http.request.header.regex_test_header_2": ( - "RegexTestValue2,RegexTestValue3", - ), - "http.request.header.my_secret_header": ("[REDACTED]",), - } - resp = self.client.get( - "/foobar", - headers={ - "custom-test-header-1": "test-header-value-1", - "custom-test-header-2": "test-header-value-2", - "Regex-Test-Header-1": "Regex Test Value 1", - "regex-test-header-2": "RegexTestValue2,RegexTestValue3", - "My-Secret-Header": "My Secret Value", - }, - ) - self.assertEqual(200, resp.status_code) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 3) - - server_span = [ - span for span in span_list if span.kind == trace.SpanKind.SERVER - ][0] - - self.assertSpanHasAttributes(server_span, expected) - - def test_http_custom_request_headers_not_in_span_attributes(self): - not_expected = { - "http.request.header.custom_test_header_3": ( - "test-header-value-3", - ), - } - resp = self.client.get( - "/foobar", - headers={ - "custom-test-header-1": "test-header-value-1", - "custom-test-header-2": "test-header-value-2", - "Regex-Test-Header-1": "Regex Test Value 1", - "regex-test-header-2": "RegexTestValue2,RegexTestValue3", - "My-Secret-Header": "My Secret Value", - }, - ) - self.assertEqual(200, resp.status_code) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 3) - - server_span = [ - span for span in span_list if span.kind == trace.SpanKind.SERVER - ][0] - - for key, _ in not_expected.items(): - self.assertNotIn(key, server_span.attributes) - - def test_http_custom_response_headers_in_span_attributes(self): - expected = { - "http.response.header.custom_test_header_1": ( - "test-header-value-1", - ), - "http.response.header.custom_test_header_2": ( - "test-header-value-2", - ), - "http.response.header.my_custom_regex_header_1": ( - "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", - ), - "http.response.header.my_secret_header": ("[REDACTED]",), - } - resp = self.client.get("/foobar") - self.assertEqual(200, resp.status_code) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 3) - - server_span = [ - span for span in span_list if span.kind == trace.SpanKind.SERVER - ][0] - self.assertSpanHasAttributes(server_span, expected) - - def test_http_custom_response_headers_not_in_span_attributes(self): - not_expected = { - "http.response.header.custom_test_header_3": ( - "test-header-value-3", - ), - } - resp = self.client.get("/foobar") - self.assertEqual(200, resp.status_code) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 3) - - server_span = [ - span for span in span_list if span.kind == trace.SpanKind.SERVER - ][0] - - for key, _ in not_expected.items(): - self.assertNotIn(key, server_span.attributes) - - -@patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", - }, -) -class TestWebSocketAppWithCustomHeaders(TestBase): - def setUp(self): - super().setUp() - self.app = self._create_app() - otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) - self.client = TestClient(self.app) - - def tearDown(self) -> None: - super().tearDown() - with self.disable_logging(): - otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) - - @staticmethod - def _create_app(): - app = fastapi.FastAPI() - - @app.websocket("/foobar_web") - async def _(websocket: fastapi.WebSocket): - message = await websocket.receive() - if message.get("type") == "websocket.connect": - await websocket.send( - { - "type": "websocket.accept", - "headers": [ - (b"custom-test-header-1", b"test-header-value-1"), - (b"custom-test-header-2", b"test-header-value-2"), - (b"Regex-Test-Header-1", b"Regex Test Value 1"), - ( - b"regex-test-header-2", - b"RegexTestValue2,RegexTestValue3", - ), - (b"My-Secret-Header", b"My Secret Value"), - ], - } - ) - await websocket.send_json({"message": "hello world"}) - await websocket.close() - if message.get("type") == "websocket.disconnect": - pass - - return app - - def test_web_socket_custom_request_headers_in_span_attributes(self): - expected = { - "http.request.header.custom_test_header_1": ( - "test-header-value-1", - ), - "http.request.header.custom_test_header_2": ( - "test-header-value-2", - ), - } - - with self.client.websocket_connect( - "/foobar_web", - headers={ - "custom-test-header-1": "test-header-value-1", - "custom-test-header-2": "test-header-value-2", - }, - ) as websocket: - data = websocket.receive_json() - self.assertEqual(data, {"message": "hello world"}) - - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 5) - - server_span = [ - span for span in span_list if span.kind == trace.SpanKind.SERVER - ][0] - - self.assertSpanHasAttributes(server_span, expected) - - @patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", - }, - ) - def test_web_socket_custom_request_headers_not_in_span_attributes(self): - not_expected = { - "http.request.header.custom_test_header_3": ( - "test-header-value-3", - ), - } - - with self.client.websocket_connect( - "/foobar_web", - headers={ - "custom-test-header-1": "test-header-value-1", - "custom-test-header-2": "test-header-value-2", - }, - ) as websocket: - data = websocket.receive_json() - self.assertEqual(data, {"message": "hello world"}) - - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 5) - - server_span = [ - span for span in span_list if span.kind == trace.SpanKind.SERVER - ][0] - - for key, _ in not_expected.items(): - self.assertNotIn(key, server_span.attributes) - - def test_web_socket_custom_response_headers_in_span_attributes(self): - expected = { - "http.response.header.custom_test_header_1": ( - "test-header-value-1", - ), - "http.response.header.custom_test_header_2": ( - "test-header-value-2", - ), - } - - with self.client.websocket_connect("/foobar_web") as websocket: - data = websocket.receive_json() - self.assertEqual(data, {"message": "hello world"}) - - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 5) - - server_span = [ - span for span in span_list if span.kind == trace.SpanKind.SERVER - ][0] - - self.assertSpanHasAttributes(server_span, expected) - - def test_web_socket_custom_response_headers_not_in_span_attributes(self): - not_expected = { - "http.response.header.custom_test_header_3": ( - "test-header-value-3", - ), - } - - with self.client.websocket_connect("/foobar_web") as websocket: - data = websocket.receive_json() - self.assertEqual(data, {"message": "hello world"}) - - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 5) - - server_span = [ - span for span in span_list if span.kind == trace.SpanKind.SERVER - ][0] - - for key, _ in not_expected.items(): - self.assertNotIn(key, server_span.attributes) - - -@patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - }, -) -class TestNonRecordingSpanWithCustomHeaders(TestBase): - def setUp(self): - super().setUp() - self.app = fastapi.FastAPI() - - @self.app.get("/foobar") - async def _(): - return {"message": "hello world"} - - reset_trace_globals() - tracer_provider = trace.NoOpTracerProvider() - trace.set_tracer_provider(tracer_provider=tracer_provider) - - self._instrumentor = otel_fastapi.FastAPIInstrumentor() - self._instrumentor.instrument_app(self.app) - self.client = TestClient(self.app) - - def tearDown(self) -> None: - super().tearDown() - with self.disable_logging(): - self._instrumentor.uninstrument_app(self.app) - - def test_custom_header_not_present_in_non_recording_span(self): - resp = self.client.get( - "/foobar", - headers={ - "custom-test-header-1": "test-header-value-1", - }, - ) - self.assertEqual(200, resp.status_code) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation_custom_headers.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation_custom_headers.py new file mode 100644 index 0000000000..e7adca735c --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation_custom_headers.py @@ -0,0 +1,381 @@ +from collections.abc import Mapping +from typing import Tuple +from unittest.mock import patch + +import fastapi +from starlette.responses import JSONResponse +from starlette.testclient import TestClient + +import opentelemetry.instrumentation.fastapi as otel_fastapi +from opentelemetry import trace +from opentelemetry.test.globals_test import reset_trace_globals +from opentelemetry.test.test_base import TestBase +from opentelemetry.util.http import ( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, +) + + +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", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) +class TestHTTPAppWithCustomHeaders(TestBase): + def setUp(self): + super().setUp() + self.app = self._create_app() + otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) + self.client = TestClient(self.app) + + def tearDown(self) -> None: + super().tearDown() + with self.disable_logging(): + otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) + + @staticmethod + def _create_app(): + app = fastapi.FastAPI() + + @app.get("/foobar") + async def _(): + 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) + + return app + + def test_http_custom_request_headers_in_span_attributes(self): + expected = { + "http.request.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.request.header.custom_test_header_2": ( + "test-header-value-2", + ), + "http.request.header.regex_test_header_1": ("Regex Test Value 1",), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), + } + resp = self.client.get( + "/foobar", + headers={ + "custom-test-header-1": "test-header-value-1", + "custom-test-header-2": "test-header-value-2", + "Regex-Test-Header-1": "Regex Test Value 1", + "regex-test-header-2": "RegexTestValue2,RegexTestValue3", + "My-Secret-Header": "My Secret Value", + }, + ) + self.assertEqual(200, resp.status_code) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 3) + + server_span = [ + span for span in span_list if span.kind == trace.SpanKind.SERVER + ][0] + + self.assertSpanHasAttributes(server_span, expected) + + def test_http_custom_request_headers_not_in_span_attributes(self): + not_expected = { + "http.request.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + resp = self.client.get( + "/foobar", + headers={ + "custom-test-header-1": "test-header-value-1", + "custom-test-header-2": "test-header-value-2", + "Regex-Test-Header-1": "Regex Test Value 1", + "regex-test-header-2": "RegexTestValue2,RegexTestValue3", + "My-Secret-Header": "My Secret Value", + }, + ) + self.assertEqual(200, resp.status_code) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 3) + + server_span = [ + span for span in span_list if span.kind == trace.SpanKind.SERVER + ][0] + + for key, _ in not_expected.items(): + self.assertNotIn(key, server_span.attributes) + + def test_http_custom_response_headers_in_span_attributes(self): + expected = { + "http.response.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.response.header.custom_test_header_2": ( + "test-header-value-2", + ), + "http.response.header.my_custom_regex_header_1": ( + "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", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), + } + resp = self.client.get("/foobar") + self.assertEqual(200, resp.status_code) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 3) + + server_span = [ + span for span in span_list if span.kind == trace.SpanKind.SERVER + ][0] + self.assertSpanHasAttributes(server_span, expected) + + def test_http_custom_response_headers_not_in_span_attributes(self): + not_expected = { + "http.response.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + resp = self.client.get("/foobar") + self.assertEqual(200, resp.status_code) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 3) + + server_span = [ + span for span in span_list if span.kind == trace.SpanKind.SERVER + ][0] + + for key, _ in not_expected.items(): + self.assertNotIn(key, server_span.attributes) + + +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) +class TestWebSocketAppWithCustomHeaders(TestBase): + def setUp(self): + super().setUp() + self.app = self._create_app() + otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) + self.client = TestClient(self.app) + + def tearDown(self) -> None: + super().tearDown() + with self.disable_logging(): + otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) + + @staticmethod + def _create_app(): + app = fastapi.FastAPI() + + @app.websocket("/foobar_web") + async def _(websocket: fastapi.WebSocket): + message = await websocket.receive() + if message.get("type") == "websocket.connect": + await websocket.send( + { + "type": "websocket.accept", + "headers": [ + (b"custom-test-header-1", b"test-header-value-1"), + (b"custom-test-header-2", b"test-header-value-2"), + (b"Regex-Test-Header-1", b"Regex Test Value 1"), + ( + b"regex-test-header-2", + b"RegexTestValue2,RegexTestValue3", + ), + (b"My-Secret-Header", b"My Secret Value"), + ], + } + ) + await websocket.send_json({"message": "hello world"}) + await websocket.close() + if message.get("type") == "websocket.disconnect": + pass + + return app + + def test_web_socket_custom_request_headers_in_span_attributes(self): + expected = { + "http.request.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.request.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + + with self.client.websocket_connect( + "/foobar_web", + headers={ + "custom-test-header-1": "test-header-value-1", + "custom-test-header-2": "test-header-value-2", + }, + ) as websocket: + data = websocket.receive_json() + self.assertEqual(data, {"message": "hello world"}) + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 5) + + server_span = [ + span for span in span_list if span.kind == trace.SpanKind.SERVER + ][0] + + self.assertSpanHasAttributes(server_span, expected) + + @patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + }, + ) + def test_web_socket_custom_request_headers_not_in_span_attributes(self): + not_expected = { + "http.request.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + + with self.client.websocket_connect( + "/foobar_web", + headers={ + "custom-test-header-1": "test-header-value-1", + "custom-test-header-2": "test-header-value-2", + }, + ) as websocket: + data = websocket.receive_json() + self.assertEqual(data, {"message": "hello world"}) + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 5) + + server_span = [ + span for span in span_list if span.kind == trace.SpanKind.SERVER + ][0] + + for key, _ in not_expected.items(): + self.assertNotIn(key, server_span.attributes) + + def test_web_socket_custom_response_headers_in_span_attributes(self): + expected = { + "http.response.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.response.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + + with self.client.websocket_connect("/foobar_web") as websocket: + data = websocket.receive_json() + self.assertEqual(data, {"message": "hello world"}) + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 5) + + server_span = [ + span for span in span_list if span.kind == trace.SpanKind.SERVER + ][0] + + self.assertSpanHasAttributes(server_span, expected) + + def test_web_socket_custom_response_headers_not_in_span_attributes(self): + not_expected = { + "http.response.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + + with self.client.websocket_connect("/foobar_web") as websocket: + data = websocket.receive_json() + self.assertEqual(data, {"message": "hello world"}) + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 5) + + server_span = [ + span for span in span_list if span.kind == trace.SpanKind.SERVER + ][0] + + for key, _ in not_expected.items(): + self.assertNotIn(key, server_span.attributes) + + +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", + }, +) +class TestNonRecordingSpanWithCustomHeaders(TestBase): + def setUp(self): + super().setUp() + self.app = fastapi.FastAPI() + + @self.app.get("/foobar") + async def _(): + return {"message": "hello world"} + + reset_trace_globals() + tracer_provider = trace.NoOpTracerProvider() + trace.set_tracer_provider(tracer_provider=tracer_provider) + + self._instrumentor = otel_fastapi.FastAPIInstrumentor() + self._instrumentor.instrument_app(self.app) + self.client = TestClient(self.app) + + def tearDown(self) -> None: + super().tearDown() + with self.disable_logging(): + self._instrumentor.uninstrument_app(self.app) + + def test_custom_header_not_present_in_non_recording_span(self): + resp = self.client.get( + "/foobar", + headers={ + "custom-test-header-1": "test-header-value-1", + }, + ) + self.assertEqual(200, resp.status_code) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation_wrapped.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation_wrapped.py new file mode 100644 index 0000000000..0b17173ac6 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation_wrapped.py @@ -0,0 +1,64 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import fastapi +from starlette.testclient import TestClient + +import opentelemetry.instrumentation.fastapi as otel_fastapi +from opentelemetry import trace +from opentelemetry.test.test_base import TestBase + + +class TestWrappedApplication(TestBase): + def setUp(self): + super().setUp() + + self.app = fastapi.FastAPI() + + @self.app.get("/foobar") + async def _(): + return {"message": "hello world"} + + otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) + self.client = TestClient(self.app) + self.tracer = self.tracer_provider.get_tracer(__name__) + + def tearDown(self) -> None: + super().tearDown() + with self.disable_logging(): + otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) + + def test_mark_span_internal_in_presence_of_span_from_other_framework(self): + with self.tracer.start_as_current_span( + "test", kind=trace.SpanKind.SERVER + ) as parent_span: + resp = self.client.get("/foobar") + self.assertEqual(200, resp.status_code) + + span_list = self.memory_exporter.get_finished_spans() + for span in span_list: + print(str(span.__class__) + ": " + str(span.__dict__)) + + # there should be 4 spans - single SERVER "test" and three INTERNAL "FastAPI" + self.assertEqual(trace.SpanKind.INTERNAL, span_list[0].kind) + self.assertEqual(trace.SpanKind.INTERNAL, span_list[1].kind) + # main INTERNAL span - child of test + self.assertEqual(trace.SpanKind.INTERNAL, span_list[2].kind) + self.assertEqual( + parent_span.context.span_id, span_list[2].parent.span_id + ) + # SERVER "test" + self.assertEqual(trace.SpanKind.SERVER, span_list[3].kind) + self.assertEqual( + parent_span.context.span_id, span_list[3].context.span_id + ) diff --git a/instrumentation/opentelemetry-instrumentation-flask/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-flask/test-requirements-0.txt index fad2f5e2b0..efa8b73f82 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/test-requirements-0.txt +++ b/instrumentation/opentelemetry-instrumentation-flask/test-requirements-0.txt @@ -11,7 +11,6 @@ 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.9.0 Werkzeug==2.3.8 diff --git a/instrumentation/opentelemetry-instrumentation-flask/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-flask/test-requirements-1.txt index 919ee6d431..46b089632f 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/test-requirements-1.txt +++ b/instrumentation/opentelemetry-instrumentation-flask/test-requirements-1.txt @@ -11,7 +11,6 @@ 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.9.0 Werkzeug==2.3.8 diff --git a/instrumentation/opentelemetry-instrumentation-flask/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-flask/test-requirements-2.txt index 16d91d2058..846cff1a80 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/test-requirements-2.txt +++ b/instrumentation/opentelemetry-instrumentation-flask/test-requirements-2.txt @@ -12,7 +12,6 @@ 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.9.0 Werkzeug==3.0.3 diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py index f50d3245a0..94437bbfd2 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py @@ -497,7 +497,7 @@ def test_flask_metrics_new_semconv(self): self.client.get("/hello/123") self.client.get("/hello/321") self.client.get("/hello/756") - duration = max(round((default_timer() - start) * 1000), 0) + duration_s = max(default_timer() - start, 0) metrics_list = self.memory_metrics_reader.get_metrics_data() number_data_point_seen = False histogram_data_point_seen = False @@ -514,7 +514,7 @@ def test_flask_metrics_new_semconv(self): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) self.assertAlmostEqual( - duration, point.sum, delta=10 + duration_s, point.sum, places=2 ) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): @@ -584,8 +584,6 @@ def test_basic_metric_success(self): "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "localhost", - "net.host.name": "localhost", - "net.host.port": 80, } self._assert_basic_metric( expected_duration_attributes, @@ -627,8 +625,6 @@ def test_basic_metric_nonstandard_http_method_success(self): "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "localhost", - "net.host.name": "localhost", - "net.host.port": 80, } self._assert_basic_metric( expected_duration_attributes, diff --git a/instrumentation/opentelemetry-instrumentation-grpc/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-grpc/test-requirements.txt index d30aa5c5e9..e1f7108cfa 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-grpc/test-requirements.txt @@ -8,7 +8,6 @@ pluggy==1.5.0 protobuf==3.20.3 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 tomli==2.0.1 typing_extensions==4.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2_grpc.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2_grpc.py index 003b68f4f1..3f848b8760 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2_grpc.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2_grpc.py @@ -12,6 +12,7 @@ def __init__(self, channel): Args: channel: A grpc.Channel. """ + # pylint: disable=invalid-name self.SimpleMethod = channel.unary_unary( "/GRPCTestServer/SimpleMethod", request_serializer=test__server__pb2.Request.SerializeToString, @@ -37,6 +38,9 @@ def __init__(self, channel): class GRPCTestServerServicer: """Missing associated documentation comment in .proto file""" + # pylint: disable=invalid-name + # pylint: disable=no-self-use + def SimpleMethod(self, request, context): """Missing associated documentation comment in .proto file""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) @@ -62,7 +66,9 @@ def BidirectionalStreamingMethod(self, request_iterator, context): raise NotImplementedError("Method not implemented!") -def add_GRPCTestServerServicer_to_server(servicer, server): +def add_GRPCTestServerServicer_to_server( + servicer, server +): # pylint: disable=invalid-name rpc_method_handlers = { "SimpleMethod": grpc.unary_unary_rpc_method_handler( servicer.SimpleMethod, @@ -95,6 +101,7 @@ def add_GRPCTestServerServicer_to_server(servicer, server): class GRPCTestServer: """Missing associated documentation comment in .proto file""" + # pylint: disable=invalid-name @staticmethod def SimpleMethod( request, 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 5404b2f025..d2ff0be292 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py @@ -196,6 +196,19 @@ async def async_response_hook(span, request, response): import httpx +from opentelemetry.instrumentation._semconv import ( + _get_schema_url, + _HTTPStabilityMode, + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _report_new, + _set_http_host, + _set_http_method, + _set_http_network_protocol_version, + _set_http_peer_port_client, + _set_http_status_code, + _set_http_url, +) from opentelemetry.instrumentation.httpx.package import _instruments from opentelemetry.instrumentation.httpx.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor @@ -204,11 +217,15 @@ async def async_response_hook(span, request, response): is_http_instrumentation_enabled, ) from opentelemetry.propagate import inject -from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE +from opentelemetry.semconv.attributes.network_attributes import ( + NETWORK_PEER_ADDRESS, + NETWORK_PEER_PORT, +) from opentelemetry.trace import SpanKind, TracerProvider, get_tracer from opentelemetry.trace.span import Span -from opentelemetry.trace.status import Status -from opentelemetry.util.http import remove_url_credentials +from opentelemetry.trace.status import StatusCode +from opentelemetry.util.http import remove_url_credentials, sanitize_method _logger = logging.getLogger(__name__) @@ -242,25 +259,11 @@ class ResponseInfo(typing.NamedTuple): def _get_default_span_name(method: str) -> str: - return method.strip() - - -def _apply_status_code(span: Span, status_code: int) -> None: - if not span.is_recording(): - return + method = sanitize_method(method.upper().strip()) + if method == "_OTHER": + method = "HTTP" - span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) - span.set_status(Status(http_status_to_status_code(status_code))) - - -def _prepare_attributes(method: bytes, url: URL) -> typing.Dict[str, str]: - _method = method.decode().upper() - _url = str(httpx.URL(url)) - span_attributes = { - SpanAttributes.HTTP_METHOD: _method, - SpanAttributes.HTTP_URL: _url, - } - return span_attributes + return method def _prepare_headers(headers: typing.Optional[Headers]) -> httpx.Headers: @@ -299,6 +302,84 @@ def _inject_propagation_headers(headers, args, kwargs): kwargs["headers"] = _headers.raw +def _extract_response( + response: typing.Union[ + httpx.Response, typing.Tuple[int, Headers, httpx.SyncByteStream, dict] + ] +) -> typing.Tuple[int, Headers, httpx.SyncByteStream, dict, str]: + if isinstance(response, httpx.Response): + status_code = response.status_code + headers = response.headers + stream = response.stream + extensions = response.extensions + http_version = response.http_version + else: + status_code, headers, stream, extensions = response + http_version = extensions.get("http_version", b"HTTP/1.1").decode( + "ascii", errors="ignore" + ) + + return (status_code, headers, stream, extensions, http_version) + + +def _apply_request_client_attributes_to_span( + span_attributes: dict, + url: typing.Union[str, URL, httpx.URL], + method_original: str, + span_name: str, + semconv: _HTTPStabilityMode, +): + url = httpx.URL(url) + # http semconv transition: http.method -> http.request.method + _set_http_method(span_attributes, method_original, span_name, semconv) + # http semconv transition: http.url -> url.full + _set_http_url(span_attributes, str(url), semconv) + + if _report_new(semconv): + if url.host: + # http semconv transition: http.host -> server.address + _set_http_host(span_attributes, url.host, semconv) + # http semconv transition: net.sock.peer.addr -> network.peer.address + span_attributes[NETWORK_PEER_ADDRESS] = url.host + if url.port: + # http semconv transition: net.sock.peer.port -> network.peer.port + _set_http_peer_port_client(span_attributes, url.port, semconv) + span_attributes[NETWORK_PEER_PORT] = url.port + + +def _apply_response_client_attributes_to_span( + span: Span, + status_code: int, + http_version: str, + semconv: _HTTPStabilityMode, +): + # http semconv transition: http.status_code -> http.response.status_code + # TODO: use _set_status when it's stable for http clients + span_attributes = {} + _set_http_status_code( + span_attributes, + status_code, + semconv, + ) + http_status_code = http_status_to_status_code(status_code) + span.set_status(http_status_code) + + if http_status_code == StatusCode.ERROR and _report_new(semconv): + # http semconv transition: new error.type + span_attributes[ERROR_TYPE] = str(status_code) + + if http_version and _report_new(semconv): + # http semconv transition: http.flavor -> network.protocol.version + _set_http_network_protocol_version( + span_attributes, + http_version.replace("HTTP/", ""), + semconv, + ) + + for key, val in span_attributes.items(): + span.set_attribute(key, val) + + class SyncOpenTelemetryTransport(httpx.BaseTransport): """Sync transport class that will trace all requests made with a client. @@ -318,12 +399,17 @@ def __init__( request_hook: typing.Optional[RequestHook] = None, response_hook: typing.Optional[ResponseHook] = None, ): + _OpenTelemetrySemanticConventionStability._initialize() + self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP, + ) + self._transport = transport self._tracer = get_tracer( __name__, instrumenting_library_version=__version__, tracer_provider=tracer_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url(self._sem_conv_opt_in_mode), ) self._request_hook = request_hook self._response_hook = response_hook @@ -340,6 +426,7 @@ def __exit__( ) -> None: self._transport.__exit__(exc_type, exc_value, traceback) + # pylint: disable=R0914 def handle_request( self, *args, @@ -355,39 +442,64 @@ def handle_request( method, url, headers, stream, extensions = _extract_parameters( args, kwargs ) - span_attributes = _prepare_attributes(method, url) + method_original = method.decode() + span_name = _get_default_span_name(method_original) + span_attributes = {} + # apply http client response attributes according to semconv + _apply_request_client_attributes_to_span( + span_attributes, + url, + method_original, + span_name, + self._sem_conv_opt_in_mode, + ) request_info = RequestInfo(method, url, headers, stream, extensions) - span_name = _get_default_span_name( - span_attributes[SpanAttributes.HTTP_METHOD] - ) with self._tracer.start_as_current_span( span_name, kind=SpanKind.CLIENT, attributes=span_attributes ) as span: - if self._request_hook is not None: + exception = None + if callable(self._request_hook): self._request_hook(span, request_info) _inject_propagation_headers(headers, args, kwargs) - response = self._transport.handle_request(*args, **kwargs) - if isinstance(response, httpx.Response): - response: httpx.Response = response - status_code = response.status_code - headers = response.headers - stream = response.stream - extensions = response.extensions - else: - status_code, headers, stream, extensions = response - - _apply_status_code(span, status_code) - - if self._response_hook is not None: - self._response_hook( - span, - request_info, - ResponseInfo(status_code, headers, stream, extensions), + + try: + response = self._transport.handle_request(*args, **kwargs) + except Exception as exc: # pylint: disable=W0703 + exception = exc + response = getattr(exc, "response", None) + + if isinstance(response, (httpx.Response, tuple)): + status_code, headers, stream, extensions, http_version = ( + _extract_response(response) ) + if span.is_recording(): + # apply http client response attributes according to semconv + _apply_response_client_attributes_to_span( + span, + status_code, + http_version, + self._sem_conv_opt_in_mode, + ) + if callable(self._response_hook): + self._response_hook( + span, + request_info, + ResponseInfo(status_code, headers, stream, extensions), + ) + + if exception: + if span.is_recording() and _report_new( + self._sem_conv_opt_in_mode + ): + span.set_attribute( + ERROR_TYPE, type(exception).__qualname__ + ) + raise exception.with_traceback(exception.__traceback__) + return response def close(self) -> None: @@ -413,12 +525,17 @@ def __init__( request_hook: typing.Optional[AsyncRequestHook] = None, response_hook: typing.Optional[AsyncResponseHook] = None, ): + _OpenTelemetrySemanticConventionStability._initialize() + self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP, + ) + self._transport = transport self._tracer = get_tracer( __name__, instrumenting_library_version=__version__, tracer_provider=tracer_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url(self._sem_conv_opt_in_mode), ) self._request_hook = request_hook self._response_hook = response_hook @@ -435,6 +552,7 @@ async def __aexit__( ) -> None: await self._transport.__aexit__(exc_type, exc_value, traceback) + # pylint: disable=R0914 async def handle_async_request(self, *args, **kwargs) -> typing.Union[ typing.Tuple[int, "Headers", httpx.AsyncByteStream, dict], httpx.Response, @@ -446,41 +564,66 @@ async def handle_async_request(self, *args, **kwargs) -> typing.Union[ method, url, headers, stream, extensions = _extract_parameters( args, kwargs ) - span_attributes = _prepare_attributes(method, url) - - span_name = _get_default_span_name( - span_attributes[SpanAttributes.HTTP_METHOD] + method_original = method.decode() + span_name = _get_default_span_name(method_original) + span_attributes = {} + # apply http client response attributes according to semconv + _apply_request_client_attributes_to_span( + span_attributes, + url, + method_original, + span_name, + self._sem_conv_opt_in_mode, ) + request_info = RequestInfo(method, url, headers, stream, extensions) with self._tracer.start_as_current_span( span_name, kind=SpanKind.CLIENT, attributes=span_attributes ) as span: - if self._request_hook is not None: + exception = None + if callable(self._request_hook): await self._request_hook(span, request_info) _inject_propagation_headers(headers, args, kwargs) - response = await self._transport.handle_async_request( - *args, **kwargs - ) - if isinstance(response, httpx.Response): - response: httpx.Response = response - status_code = response.status_code - headers = response.headers - stream = response.stream - extensions = response.extensions - else: - status_code, headers, stream, extensions = response - - _apply_status_code(span, status_code) - - if self._response_hook is not None: - await self._response_hook( - span, - request_info, - ResponseInfo(status_code, headers, stream, extensions), + try: + response = await self._transport.handle_async_request( + *args, **kwargs ) + except Exception as exc: # pylint: disable=W0703 + exception = exc + response = getattr(exc, "response", None) + + if isinstance(response, (httpx.Response, tuple)): + status_code, headers, stream, extensions, http_version = ( + _extract_response(response) + ) + + if span.is_recording(): + # apply http client response attributes according to semconv + _apply_response_client_attributes_to_span( + span, + status_code, + http_version, + self._sem_conv_opt_in_mode, + ) + + if callable(self._response_hook): + await self._response_hook( + span, + request_info, + ResponseInfo(status_code, headers, stream, extensions), + ) + + if exception: + if span.is_recording() and _report_new( + self._sem_conv_opt_in_mode + ): + span.set_attribute( + ERROR_TYPE, type(exception).__qualname__ + ) + raise exception.with_traceback(exception.__traceback__) return response diff --git a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/package.py b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/package.py index 4e548655b6..633e01c8b1 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/package.py +++ b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/package.py @@ -14,3 +14,7 @@ _instruments = ("httpx >= 0.18.0",) + +_supports_metrics = False + +_semconv_status = "migration" diff --git a/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-0.txt index ca3a0908fa..a9f1a2aaef 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-0.txt +++ b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-0.txt @@ -13,7 +13,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 respx==0.17.1 rfc3986==1.5.0 sniffio==1.3.1 diff --git a/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1.txt index d3476cea4b..928f5d8621 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1.txt +++ b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1.txt @@ -13,7 +13,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 respx==0.20.2 sniffio==1.3.1 tomli==2.0.1 diff --git a/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py b/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py index 06ad963ab0..84bab598e6 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py +++ b/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=too-many-lines + import abc import asyncio import typing @@ -22,6 +24,10 @@ import opentelemetry.instrumentation.httpx from opentelemetry import trace +from opentelemetry.instrumentation._semconv import ( + OTEL_SEMCONV_STABILITY_OPT_IN, + _OpenTelemetrySemanticConventionStability, +) from opentelemetry.instrumentation.httpx import ( AsyncOpenTelemetryTransport, HTTPXClientInstrumentor, @@ -30,6 +36,21 @@ 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.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 @@ -100,6 +121,9 @@ async def _async_no_update_request_hook(span: "Span", request: "RequestInfo"): return 123 +# pylint: disable=too-many-public-methods + + # Using this wrapper class to have a base class for the tests while also not # angering pylint or mypy when calling methods not in the class when only # subclassing abc.ABC. @@ -112,15 +136,39 @@ class BaseTest(TestBase, metaclass=abc.ABCMeta): request_hook = staticmethod(_request_hook) no_update_request_hook = staticmethod(_no_update_request_hook) + # TODO: make this more explicit to tests # pylint: disable=invalid-name def setUp(self): super().setUp() + test_name = "" + if hasattr(self, "_testMethodName"): + test_name = self._testMethodName + sem_conv_mode = "default" + if "new_semconv" in test_name: + sem_conv_mode = "http" + elif "both_semconv" in test_name: + sem_conv_mode = "http/dup" + self.env_patch = mock.patch.dict( + "os.environ", + { + OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, + }, + ) + self.env_patch.start() + _OpenTelemetrySemanticConventionStability._initialized = False respx.start() - respx.get(self.URL).mock(httpx.Response(200, text="Hello!")) + respx.get(self.URL).mock( + httpx.Response( + 200, + text="Hello!", + extensions={"http_version": b"HTTP/1.1"}, + ) + ) # pylint: disable=invalid-name def tearDown(self): super().tearDown() + self.env_patch.stop() respx.stop() def assert_span( @@ -169,6 +217,87 @@ def test_basic(self): span, opentelemetry.instrumentation.httpx ) + def test_basic_new_semconv(self): + url = "http://mock:8080/status/200" + respx.get(url).mock( + httpx.Response( + 200, + text="Hello!", + extensions={"http_version": b"HTTP/1.1"}, + ) + ) + result = self.perform_request(url) + self.assertEqual(result.text, "Hello!") + span = self.assert_span() + + self.assertIs(span.kind, trace.SpanKind.CLIENT) + self.assertEqual(span.name, "GET") + + self.assertEqual( + span.instrumentation_scope.schema_url, + SpanAttributes.SCHEMA_URL, + ) + self.assertEqual( + span.attributes, + { + HTTP_REQUEST_METHOD: "GET", + URL_FULL: url, + SERVER_ADDRESS: "mock", + NETWORK_PEER_ADDRESS: "mock", + HTTP_RESPONSE_STATUS_CODE: 200, + NETWORK_PROTOCOL_VERSION: "1.1", + SERVER_PORT: 8080, + NETWORK_PEER_PORT: 8080, + }, + ) + + self.assertIs(span.status.status_code, trace.StatusCode.UNSET) + + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.httpx + ) + + def test_basic_both_semconv(self): + url = "http://mock:8080/status/200" # 8080 because httpx returns None for common ports (http, https, wss) + respx.get(url).mock(httpx.Response(200, text="Hello!")) + result = self.perform_request(url) + self.assertEqual(result.text, "Hello!") + span = self.assert_span() + + self.assertIs(span.kind, trace.SpanKind.CLIENT) + self.assertEqual(span.name, "GET") + + self.assertEqual( + span.instrumentation_scope.schema_url, + SpanAttributes.SCHEMA_URL, + ) + + self.assertEqual( + span.attributes, + { + SpanAttributes.HTTP_METHOD: "GET", + HTTP_REQUEST_METHOD: "GET", + SpanAttributes.HTTP_URL: url, + URL_FULL: url, + SpanAttributes.HTTP_HOST: "mock", + SERVER_ADDRESS: "mock", + NETWORK_PEER_ADDRESS: "mock", + SpanAttributes.NET_PEER_PORT: 8080, + SpanAttributes.HTTP_STATUS_CODE: 200, + HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.HTTP_FLAVOR: "1.1", + NETWORK_PROTOCOL_VERSION: "1.1", + SERVER_PORT: 8080, + NETWORK_PEER_PORT: 8080, + }, + ) + + self.assertIs(span.status.status_code, trace.StatusCode.UNSET) + + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.httpx + ) + def test_basic_multiple(self): self.perform_request(self.URL) self.perform_request(self.URL) @@ -191,6 +320,48 @@ def test_not_foundbasic(self): trace.StatusCode.ERROR, ) + def test_not_foundbasic_new_semconv(self): + url_404 = "http://mock/status/404" + + with respx.mock: + respx.get(url_404).mock(httpx.Response(404)) + result = self.perform_request(url_404) + + self.assertEqual(result.status_code, 404) + span = self.assert_span() + self.assertEqual( + span.attributes.get(HTTP_RESPONSE_STATUS_CODE), 404 + ) + # new in semconv + self.assertEqual(span.attributes.get(ERROR_TYPE), "404") + + self.assertIs( + span.status.status_code, + trace.StatusCode.ERROR, + ) + + def test_not_foundbasic_both_semconv(self): + url_404 = "http://mock/status/404" + + with respx.mock: + respx.get(url_404).mock(httpx.Response(404)) + result = self.perform_request(url_404) + + self.assertEqual(result.status_code, 404) + span = self.assert_span() + self.assertEqual( + span.attributes.get(SpanAttributes.HTTP_STATUS_CODE), 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, + trace.StatusCode.ERROR, + ) + def test_suppress_instrumentation(self): with suppress_http_instrumentation(): result = self.perform_request(self.URL) @@ -245,6 +416,83 @@ def test_requests_basic_exception(self): span = self.assert_span() self.assertEqual(span.status.status_code, StatusCode.ERROR) + self.assertIn("Exception", span.status.description) + self.assertEqual( + span.events[0].attributes["exception.type"], "Exception" + ) + self.assertIsNone(span.attributes.get(ERROR_TYPE)) + + def test_requests_basic_exception_new_semconv(self): + with respx.mock, self.assertRaises(Exception): + respx.get(self.URL).mock(side_effect=Exception) + self.perform_request(self.URL) + + span = self.assert_span() + self.assertEqual(span.status.status_code, StatusCode.ERROR) + self.assertIn("Exception", span.status.description) + self.assertEqual( + span.events[0].attributes["exception.type"], "Exception" + ) + self.assertEqual(span.attributes.get(ERROR_TYPE), "Exception") + + def test_requests_basic_exception_both_semconv(self): + with respx.mock, self.assertRaises(Exception): + respx.get(self.URL).mock(side_effect=Exception) + self.perform_request(self.URL) + + span = self.assert_span() + self.assertEqual(span.status.status_code, StatusCode.ERROR) + self.assertIn("Exception", span.status.description) + self.assertEqual( + span.events[0].attributes["exception.type"], "Exception" + ) + self.assertEqual(span.attributes.get(ERROR_TYPE), "Exception") + + def test_requests_timeout_exception_new_semconv(self): + url = "http://mock:8080/exception" + with respx.mock, self.assertRaises(httpx.TimeoutException): + respx.get(url).mock(side_effect=httpx.TimeoutException) + self.perform_request(url) + + span = self.assert_span() + self.assertEqual( + span.attributes, + { + HTTP_REQUEST_METHOD: "GET", + URL_FULL: url, + SERVER_ADDRESS: "mock", + SERVER_PORT: 8080, + NETWORK_PEER_PORT: 8080, + NETWORK_PEER_ADDRESS: "mock", + ERROR_TYPE: "TimeoutException", + }, + ) + self.assertEqual(span.status.status_code, StatusCode.ERROR) + + def test_requests_timeout_exception_both_semconv(self): + url = "http://mock:8080/exception" + with respx.mock, self.assertRaises(httpx.TimeoutException): + respx.get(url).mock(side_effect=httpx.TimeoutException) + self.perform_request(url) + + span = self.assert_span() + self.assertEqual( + span.attributes, + { + SpanAttributes.HTTP_METHOD: "GET", + HTTP_REQUEST_METHOD: "GET", + SpanAttributes.HTTP_URL: url, + URL_FULL: url, + SpanAttributes.HTTP_HOST: "mock", + SERVER_ADDRESS: "mock", + NETWORK_PEER_ADDRESS: "mock", + SpanAttributes.NET_PEER_PORT: 8080, + SERVER_PORT: 8080, + NETWORK_PEER_PORT: 8080, + ERROR_TYPE: "TimeoutException", + }, + ) + self.assertEqual(span.status.status_code, StatusCode.ERROR) def test_requests_timeout_exception(self): with respx.mock, self.assertRaises(httpx.TimeoutException): @@ -373,6 +621,28 @@ def test_not_recording(self): self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) + @respx.mock + def test_not_recording_not_set_attribute_in_exception_new_semconv( + self, + ): + respx.get(self.URL).mock(side_effect=httpx.TimeoutException) + with mock.patch("opentelemetry.trace.INVALID_SPAN") as mock_span: + transport = self.create_transport( + tracer_provider=trace.NoOpTracerProvider() + ) + client = self.create_client(transport) + mock_span.is_recording.return_value = False + try: + self.perform_request(self.URL, client=client) + except httpx.TimeoutException: + pass + + self.assert_span(None, 0) + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + class BaseInstrumentorTest(BaseTest, metaclass=abc.ABCMeta): @abc.abstractmethod def create_client( diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-jinja2/test-requirements.txt index e547f9bd20..4978ab40a7 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-jinja2/test-requirements.txt @@ -8,7 +8,6 @@ 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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.txt index 2f7007f872..6a9909e570 100644 --- a/instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.txt @@ -7,7 +7,6 @@ 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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-logging/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-logging/test-requirements.txt index b7fcdc3dee..e085e96c73 100644 --- a/instrumentation/opentelemetry-instrumentation-logging/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-logging/test-requirements.txt @@ -6,7 +6,6 @@ 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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-mysql/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-mysql/test-requirements.txt index 95cd6ab35f..889bfc50da 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-mysql/test-requirements.txt @@ -7,7 +7,6 @@ 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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-mysqlclient/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-mysqlclient/test-requirements.txt index 78060fbccc..d6c6eba35f 100644 --- a/instrumentation/opentelemetry-instrumentation-mysqlclient/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-mysqlclient/test-requirements.txt @@ -7,7 +7,6 @@ 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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-pika/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-pika/test-requirements-0.txt index b6aa239e0f..093ca640b7 100644 --- a/instrumentation/opentelemetry-instrumentation-pika/test-requirements-0.txt +++ b/instrumentation/opentelemetry-instrumentation-pika/test-requirements-0.txt @@ -7,7 +7,6 @@ pika==0.13.1 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 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-pika/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-pika/test-requirements-1.txt index 334d08f537..7e7a31ff52 100644 --- a/instrumentation/opentelemetry-instrumentation-pika/test-requirements-1.txt +++ b/instrumentation/opentelemetry-instrumentation-pika/test-requirements-1.txt @@ -7,7 +7,6 @@ pika==1.3.2 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 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py index 5d7054151a..4f61713b29 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py @@ -269,7 +269,8 @@ def get_operation_name(self, cursor, args): if isinstance(statement, Composed): statement = statement.as_string(cursor) - if isinstance(statement, str): + # `statement` can be empty string. See #2643 + if statement and isinstance(statement, str): # Strip leading comments so we get the operation name. return self._leading_comment_remover.sub("", statement).split()[0] diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-0.txt index d9e9b4de0b..ef2c7742a1 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-0.txt +++ b/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-0.txt @@ -8,7 +8,6 @@ pluggy==1.5.0 psycopg==3.1.18 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 tomli==2.0.1 typing_extensions==4.10.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-1.txt index 9269a3c378..f857b2b939 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-1.txt +++ b/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-1.txt @@ -7,7 +7,6 @@ pluggy==1.5.0 psycopg==3.1.18 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 tomli==2.0.1 typing_extensions==4.10.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py index 5a5b39d80b..dc9969ba8c 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py @@ -245,14 +245,18 @@ def test_span_name(self): cursor.execute("/* leading comment */ query") cursor.execute("/* leading comment */ query /* trailing comment */") cursor.execute("query /* trailing comment */") + cursor.execute("") + cursor.execute("--") spans_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans_list), 6) + self.assertEqual(len(spans_list), 8) self.assertEqual(spans_list[0].name, "Test") self.assertEqual(spans_list[1].name, "multi") self.assertEqual(spans_list[2].name, "tab") self.assertEqual(spans_list[3].name, "query") self.assertEqual(spans_list[4].name, "query") self.assertEqual(spans_list[5].name, "query") + self.assertEqual(spans_list[6].name, "postgresql") + self.assertEqual(spans_list[7].name, "--") # pylint: disable=unused-argument def test_not_recording(self): diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-psycopg2/test-requirements.txt index 28ad25715d..2424afe1ff 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/test-requirements.txt @@ -7,7 +7,6 @@ pluggy==1.5.0 psycopg2==2.9.9 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 tomli==2.0.1 typing_extensions==4.10.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-0.txt index ddb06914f7..ebe17d4af6 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-0.txt +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-0.txt @@ -7,7 +7,6 @@ pluggy==1.5.0 py-cpuinfo==9.0.0 pymemcache==1.3.5 pytest==7.4.4 -pytest-benchmark==4.0.0 six==1.16.0 tomli==2.0.1 typing_extensions==4.10.0 diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-1.txt index a6ad4d0248..5f1676077e 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-1.txt +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-1.txt @@ -7,7 +7,6 @@ pluggy==1.5.0 py-cpuinfo==9.0.0 pymemcache==2.2.2 pytest==7.4.4 -pytest-benchmark==4.0.0 six==1.16.0 tomli==2.0.1 typing_extensions==4.10.0 diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-2.txt index 7405224a8d..e3887cb8b1 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-2.txt +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-2.txt @@ -7,7 +7,6 @@ pluggy==1.5.0 py-cpuinfo==9.0.0 pymemcache==3.4.1 pytest==7.4.4 -pytest-benchmark==4.0.0 six==1.16.0 tomli==2.0.1 typing_extensions==4.10.0 diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-3.txt b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-3.txt index d817e70c59..26c4ebe02b 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-3.txt +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-3.txt @@ -7,7 +7,6 @@ pluggy==1.5.0 py-cpuinfo==9.0.0 pymemcache==3.4.2 pytest==7.4.4 -pytest-benchmark==4.0.0 six==1.16.0 tomli==2.0.1 typing_extensions==4.10.0 diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-4.txt b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-4.txt index 606d79143c..cd80efe831 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-4.txt +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-4.txt @@ -7,7 +7,6 @@ pluggy==1.5.0 py-cpuinfo==9.0.0 pymemcache==4.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 tomli==2.0.1 typing_extensions==4.10.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt index 2fd3f4ed0e..32f46253ad 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt @@ -8,7 +8,6 @@ pluggy==1.5.0 py-cpuinfo==9.0.0 pymongo==4.6.3 pytest==7.4.4 -pytest-benchmark==4.0.0 tomli==2.0.1 typing_extensions==4.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt index 5f2d8b7783..6f5fe6b9af 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt @@ -7,7 +7,6 @@ pluggy==1.5.0 py-cpuinfo==9.0.0 PyMySQL==1.1.1 pytest==7.4.4 -pytest-benchmark==4.0.0 tomli==2.0.1 typing_extensions==4.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt index 184b03fed4..ce646b601b 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt @@ -11,7 +11,6 @@ pluggy==1.5.0 py-cpuinfo==9.0.0 pyramid==2.0.2 pytest==7.4.4 -pytest-benchmark==4.0.0 tomli==2.0.1 translationstring==1.4 typing_extensions==4.9.0 diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py index b1d854b371..b40cf3355a 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py @@ -224,8 +224,6 @@ def test_basic_metric_success(self): "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "localhost", - "net.host.name": "localhost", - "net.host.port": 80, } metrics_list = self.memory_metrics_reader.get_metrics_data() for metric in ( diff --git a/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt index 00b0a04b3e..3228d08752 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt @@ -7,7 +7,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 redis==5.0.1 tomli==2.0.1 typing_extensions==4.9.0 diff --git a/instrumentation/opentelemetry-instrumentation-remoulade/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-remoulade/test-requirements.txt index 0f6374b7ef..680df4cfa6 100644 --- a/instrumentation/opentelemetry-instrumentation-remoulade/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-remoulade/test-requirements.txt @@ -7,7 +7,6 @@ pluggy==1.5.0 prometheus_client==0.20.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 python-dateutil==2.8.2 pytz==2024.1 remoulade==3.2.0 diff --git a/instrumentation/opentelemetry-instrumentation-requests/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-requests/test-requirements.txt index c5c8f84366..406e1d6c02 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-requests/test-requirements.txt @@ -10,7 +10,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 requests==2.32.3 tomli==2.0.1 typing_extensions==4.9.0 diff --git a/instrumentation/opentelemetry-instrumentation-sklearn/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-sklearn/test-requirements.txt index 885caae033..760871c1ec 100644 --- a/instrumentation/opentelemetry-instrumentation-sklearn/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-sklearn/test-requirements.txt @@ -8,7 +8,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 scikit-learn==0.24.2 scipy==1.10.1 threadpoolctl==3.3.0 diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-0.txt index 04f4fe0c4b..f5a7c5243c 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-0.txt +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-0.txt @@ -9,7 +9,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 readline==6.2.4.1 SQLAlchemy==1.1.18 tomli==2.0.1 diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-1.txt index 4e1620b772..63272a878e 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-1.txt +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-1.txt @@ -8,7 +8,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 SQLAlchemy==1.4.51 tomli==2.0.1 typing_extensions==4.10.0 diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-sqlite3/test-requirements.txt index 848f207c8c..f7db73a853 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/test-requirements.txt @@ -6,7 +6,6 @@ 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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.txt index a582353901..27d107299d 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.txt @@ -14,7 +14,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 requests==2.32.3 sniffio==1.3.0 starlette==0.13.8 diff --git a/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py b/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py index 1e768982b5..eed1a75c44 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py @@ -18,7 +18,7 @@ from starlette import applications from starlette.responses import PlainTextResponse -from starlette.routing import Route +from starlette.routing import Mount, Route from starlette.testclient import TestClient from starlette.websockets import WebSocket @@ -103,6 +103,43 @@ def test_basic_starlette_call(self): "opentelemetry.instrumentation.starlette", ) + def test_sub_app_starlette_call(self): + """ + This test is to ensure that a span in case of a sub app targeted contains the correct server url + """ + + self._client.get("/sub/home") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 3) + for span in spans: + # As we are only looking to the "outer" app, we would see only the "GET /sub" spans + self.assertIn("GET /sub", span.name) + + # We now want to specifically test all spans including the + # - HTTP_TARGET + # - HTTP_URL + # attributes to be populated with the expected values + spans_with_http_attributes = [ + span + for span in spans + if ( + SpanAttributes.HTTP_URL in span.attributes + or SpanAttributes.HTTP_TARGET in span.attributes + ) + ] + + # expect only one span to have the attributes + self.assertEqual(1, len(spans_with_http_attributes)) + + for span in spans_with_http_attributes: + self.assertEqual( + "/sub/home", span.attributes[SpanAttributes.HTTP_TARGET] + ) + self.assertEqual( + "http://testserver/sub/home", + span.attributes[SpanAttributes.HTTP_URL], + ) + def test_starlette_route_attribute_added(self): """Ensure that starlette routes are used as the span name.""" self._client.get("/user/123") @@ -250,13 +287,20 @@ def home(_): def health(_): return PlainTextResponse("ok") + def sub_home(_): + return PlainTextResponse("sub hi") + + sub_app = applications.Starlette(routes=[Route("/home", sub_home)]) + app = applications.Starlette( routes=[ Route("/foobar", home), Route("/user/{username}", home), Route("/healthzz", health), - ] + Mount("/sub", app=sub_app), + ], ) + return app @@ -354,6 +398,72 @@ def test_uninstrument(self): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) + def test_sub_app_starlette_call(self): + """ + !!! Attention: we need to override this testcase for the auto-instrumented variant + The reason is, that with auto instrumentation, the sub app is instrumented as well + and therefore we would see the spans for the sub app as well + + This test is to ensure that a span in case of a sub app targeted contains the correct server url + """ + + self._client.get("/sub/home") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 6) + + for span in spans: + # As we are only looking to the "outer" app, we would see only the "GET /sub" spans + # -> the outer app is not aware of the sub_apps internal routes + sub_in = "GET /sub" in span.name + # The sub app spans are named GET /home as from the sub app perspective the request targets /home + # -> the sub app is technically not aware of the /sub prefix + home_in = "GET /home" in span.name + + # We expect the spans to be either from the outer app or the sub app + self.assertTrue( + sub_in or home_in, + f"Span {span.name} does not have /sub or /home in its name", + ) + + # We now want to specifically test all spans including the + # - HTTP_TARGET + # - HTTP_URL + # attributes to be populated with the expected values + spans_with_http_attributes = [ + span + for span in spans + if ( + SpanAttributes.HTTP_URL in span.attributes + or SpanAttributes.HTTP_TARGET in span.attributes + ) + ] + + # We now expect spans with attributes from both the app and its sub app + self.assertEqual(2, len(spans_with_http_attributes)) + + # Due to a potential bug in starlettes handling of sub app mounts, we can + # check only the server kind spans for the correct attributes + # The internal one generated by the sub app is not yet producing the correct attributes + server_span = next( + ( + span + for span in spans_with_http_attributes + if span.kind == SpanKind.SERVER + ), + None, + ) + + self.assertIsNotNone(server_span) + # As soon as the bug is fixed for starlette, we can iterate over spans_with_http_attributes here + # to verify the correctness of the attributes for the internal span as well + self.assertEqual( + "/sub/home", server_span.attributes[SpanAttributes.HTTP_TARGET] + ) + self.assertEqual( + "http://testserver/sub/home", + server_span.attributes[SpanAttributes.HTTP_URL], + ) + class TestAutoInstrumentationHooks(TestStarletteManualInstrumentationHooks): """ @@ -374,6 +484,72 @@ def tearDown(self): self._instrumentor.uninstrument() super().tearDown() + def test_sub_app_starlette_call(self): + """ + !!! Attention: we need to override this testcase for the auto-instrumented variant + The reason is, that with auto instrumentation, the sub app is instrumented as well + and therefore we would see the spans for the sub app as well + + This test is to ensure that a span in case of a sub app targeted contains the correct server url + """ + + self._client.get("/sub/home") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 6) + + for span in spans: + # As we are only looking to the "outer" app, we would see only the "GET /sub" spans + # -> the outer app is not aware of the sub_apps internal routes + sub_in = "GET /sub" in span.name + # The sub app spans are named GET /home as from the sub app perspective the request targets /home + # -> the sub app is technically not aware of the /sub prefix + home_in = "GET /home" in span.name + + # We expect the spans to be either from the outer app or the sub app + self.assertTrue( + sub_in or home_in, + f"Span {span.name} does not have /sub or /home in its name", + ) + + # We now want to specifically test all spans including the + # - HTTP_TARGET + # - HTTP_URL + # attributes to be populated with the expected values + spans_with_http_attributes = [ + span + for span in spans + if ( + SpanAttributes.HTTP_URL in span.attributes + or SpanAttributes.HTTP_TARGET in span.attributes + ) + ] + + # We now expect spans with attributes from both the app and its sub app + self.assertEqual(2, len(spans_with_http_attributes)) + + # Due to a potential bug in starlettes handling of sub app mounts, we can + # check only the server kind spans for the correct attributes + # The internal one generated by the sub app is not yet producing the correct attributes + server_span = next( + ( + span + for span in spans_with_http_attributes + if span.kind == SpanKind.SERVER + ), + None, + ) + + self.assertIsNotNone(server_span) + # As soon as the bug is fixed for starlette, we can iterate over spans_with_http_attributes here + # to verify the correctness of the attributes for the internal span as well + self.assertEqual( + "/sub/home", server_span.attributes[SpanAttributes.HTTP_TARGET] + ) + self.assertEqual( + "http://testserver/sub/home", + server_span.attributes[SpanAttributes.HTTP_URL], + ) + class TestAutoInstrumentationLogic(unittest.TestCase): def test_instrumentation(self): diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py index 6342d287d5..b7ffb25431 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py @@ -117,6 +117,7 @@ "process.runtime.thread_count": None, "process.runtime.cpu.utilization": None, "process.runtime.context_switches": ["involuntary", "voluntary"], + "process.open_file_descriptor.count": None, } if sys.platform == "darwin": @@ -169,6 +170,7 @@ def __init__( self._runtime_thread_count_labels = self._labels.copy() self._runtime_cpu_utilization_labels = self._labels.copy() self._runtime_context_switches_labels = self._labels.copy() + self._open_file_descriptor_count_labels = self._labels.copy() def instrumentation_dependencies(self) -> Collection[str]: return _instruments @@ -395,9 +397,25 @@ def _instrument(self, **kwargs): unit="switches", ) + if "process.open_file_descriptor.count" in self._config: + self._meter.create_observable_up_down_counter( + name="process.open_file_descriptor.count", + callbacks=[self._get_open_file_descriptors], + description="Number of file descriptors in use by the process.", + ) + def _uninstrument(self, **__): pass + def _get_open_file_descriptors( + self, options: CallbackOptions + ) -> Iterable[Observation]: + """Observer callback for Number of file descriptors in use by the process""" + yield Observation( + self._proc.num_fds(), + self._open_file_descriptor_count_labels.copy(), + ) + def _get_system_cpu_time( self, options: CallbackOptions ) -> Iterable[Observation]: diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-system-metrics/test-requirements.txt index 13573c65bd..943bdcc90d 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/test-requirements.txt @@ -7,7 +7,6 @@ pluggy==1.5.0 psutil==6.0.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 tomli==2.0.1 typing_extensions==4.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py index 3986a32c16..1d6f08892e 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py @@ -118,12 +118,13 @@ def test_system_metrics_instrument(self): f"process.runtime.{self.implementation}.thread_count", f"process.runtime.{self.implementation}.context_switches", f"process.runtime.{self.implementation}.cpu.utilization", + "process.open_file_descriptor.count", ] if self.implementation == "pypy": - self.assertEqual(len(metric_names), 20) - else: self.assertEqual(len(metric_names), 21) + else: + self.assertEqual(len(metric_names), 22) observer_names.append( f"process.runtime.{self.implementation}.gc_count", ) @@ -842,3 +843,14 @@ def test_runtime_cpu_percent(self, mock_process_cpu_percent): self._test_metrics( f"process.runtime.{self.implementation}.cpu.utilization", expected ) + + @mock.patch("psutil.Process.num_fds") + def test_open_file_descriptor_count(self, mock_process_num_fds): + mock_process_num_fds.configure_mock(**{"return_value": 3}) + + expected = [_SystemMetricsResult({}, 3)] + self._test_metrics( + "process.open_file_descriptor.count", + expected, + ) + mock_process_num_fds.assert_called() diff --git a/instrumentation/opentelemetry-instrumentation-threading/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-threading/test-requirements.txt index eb37259669..5526083a62 100644 --- a/instrumentation/opentelemetry-instrumentation-threading/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-threading/test-requirements.txt @@ -6,7 +6,6 @@ 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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt index 14ac028083..6318a7d8c6 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt @@ -16,7 +16,6 @@ packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pytest==7.4.4 -pytest-benchmark==4.0.0 requests==2.32.3 tomli==2.0.1 tornado==6.4.1 diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-tortoiseorm/test-requirements.txt index 2d518c1192..0e9713f3f6 100644 --- a/instrumentation/opentelemetry-instrumentation-tortoiseorm/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/test-requirements.txt @@ -12,7 +12,6 @@ pydantic==2.6.2 pydantic_core==2.16.3 pypika-tortoise==0.1.6 pytest==7.4.4 -pytest-benchmark==4.0.0 pytz==2024.1 tomli==2.0.1 tortoise-orm==0.20.0 diff --git a/instrumentation/opentelemetry-instrumentation-urllib/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-urllib/test-requirements.txt index 0ac7842086..c60ea23c02 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-urllib/test-requirements.txt @@ -7,7 +7,6 @@ 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.9.0 wrapt==1.16.0 diff --git a/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-0.txt index 6eb6272dbf..c3c7761ed3 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-0.txt +++ b/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-0.txt @@ -7,7 +7,6 @@ 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 urllib3==1.26.19 diff --git a/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt index 402beb85b9..783be267b1 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt +++ b/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt @@ -7,7 +7,6 @@ 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 urllib3==2.2.2 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 6a1883fa7e..d75147d6aa 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py @@ -494,8 +494,8 @@ def add_response_attributes( _set_status( span, duration_attrs, - status_code_str, status_code, + status_code_str, sem_conv_opt_in_mode, ) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-wsgi/test-requirements.txt index a25f8882d1..9c357965bf 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-wsgi/test-requirements.txt @@ -6,7 +6,6 @@ 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.9.0 wrapt==1.16.0 diff --git a/opentelemetry-distro/test-requirements.txt b/opentelemetry-distro/test-requirements.txt index b93758ee31..8cea2c2a3a 100644 --- a/opentelemetry-distro/test-requirements.txt +++ b/opentelemetry-distro/test-requirements.txt @@ -6,7 +6,6 @@ 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 wrapt==1.16.0 diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index baa06ff99b..85b8e2e3ec 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -17,6 +17,10 @@ from enum import Enum from opentelemetry.instrumentation.utils import http_status_to_status_code +from opentelemetry.semconv.attributes.client_attributes import ( + CLIENT_ADDRESS, + CLIENT_PORT, +) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, @@ -33,11 +37,18 @@ ) from opentelemetry.semconv.attributes.url_attributes import ( URL_FULL, + URL_PATH, + URL_QUERY, URL_SCHEME, ) +from opentelemetry.semconv.attributes.user_agent_attributes import ( + USER_AGENT_ORIGINAL, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace.status import Status, StatusCode +# These lists represent attributes for metrics that are currently supported + _client_duration_attrs_old = [ SpanAttributes.HTTP_STATUS_CODE, SpanAttributes.HTTP_HOST, @@ -85,13 +96,12 @@ SpanAttributes.HTTP_SCHEME, SpanAttributes.HTTP_FLAVOR, SpanAttributes.HTTP_SERVER_NAME, - SpanAttributes.NET_HOST_NAME, - SpanAttributes.NET_HOST_PORT, ] _server_active_requests_count_attrs_new = [ HTTP_REQUEST_METHOD, URL_SCHEME, + # TODO: Support SERVER_ADDRESS AND SERVER_PORT ] OTEL_SEMCONV_STABILITY_OPT_IN = "OTEL_SEMCONV_STABILITY_OPT_IN" @@ -280,14 +290,14 @@ def _set_http_net_host(result, host, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, SpanAttributes.NET_HOST_NAME, host) if _report_new(sem_conv_opt_in_mode): - set_string_attribute(result, SpanAttributes.SERVER_ADDRESS, host) + set_string_attribute(result, SERVER_ADDRESS, host) def _set_http_net_host_port(result, port, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): set_int_attribute(result, SpanAttributes.NET_HOST_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_target(result, target, path, query, sem_conv_opt_in_mode): @@ -295,23 +305,23 @@ def _set_http_target(result, target, path, query, sem_conv_opt_in_mode): set_string_attribute(result, SpanAttributes.HTTP_TARGET, target) if _report_new(sem_conv_opt_in_mode): if path: - set_string_attribute(result, SpanAttributes.URL_PATH, path) + set_string_attribute(result, URL_PATH, path) if query: - set_string_attribute(result, SpanAttributes.URL_QUERY, query) + set_string_attribute(result, URL_QUERY, query) def _set_http_peer_ip(result, ip, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, SpanAttributes.NET_PEER_IP, ip) if _report_new(sem_conv_opt_in_mode): - set_string_attribute(result, SpanAttributes.CLIENT_ADDRESS, ip) + set_string_attribute(result, CLIENT_ADDRESS, ip) def _set_http_peer_port_server(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.CLIENT_PORT, port) + set_int_attribute(result, CLIENT_PORT, port) def _set_http_user_agent(result, user_agent, sem_conv_opt_in_mode): @@ -320,32 +330,28 @@ def _set_http_user_agent(result, user_agent, sem_conv_opt_in_mode): result, SpanAttributes.HTTP_USER_AGENT, user_agent ) if _report_new(sem_conv_opt_in_mode): - set_string_attribute( - result, SpanAttributes.USER_AGENT_ORIGINAL, user_agent - ) + set_string_attribute(result, USER_AGENT_ORIGINAL, user_agent) def _set_http_net_peer_name_server(result, name, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, SpanAttributes.NET_PEER_NAME, name) if _report_new(sem_conv_opt_in_mode): - set_string_attribute(result, SpanAttributes.CLIENT_ADDRESS, name) + set_string_attribute(result, CLIENT_ADDRESS, name) def _set_http_flavor_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) def _set_status( span, metrics_attributes, - status_code_str, status_code, + status_code_str, sem_conv_opt_in_mode, ): if status_code < 0: @@ -366,12 +372,8 @@ def _set_status( span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) metrics_attributes[SpanAttributes.HTTP_STATUS_CODE] = status_code if _report_new(sem_conv_opt_in_mode): - span.set_attribute( - SpanAttributes.HTTP_RESPONSE_STATUS_CODE, status_code - ) - metrics_attributes[SpanAttributes.HTTP_RESPONSE_STATUS_CODE] = ( - status_code - ) + span.set_attribute(HTTP_RESPONSE_STATUS_CODE, status_code) + metrics_attributes[HTTP_RESPONSE_STATUS_CODE] = status_code if status == StatusCode.ERROR: span.set_attribute(ERROR_TYPE, status_code_str) metrics_attributes[ERROR_TYPE] = status_code_str diff --git a/opentelemetry-instrumentation/test-requirements.txt b/opentelemetry-instrumentation/test-requirements.txt index 2ef62218b1..f86c98d004 100644 --- a/opentelemetry-instrumentation/test-requirements.txt +++ b/opentelemetry-instrumentation/test-requirements.txt @@ -6,7 +6,6 @@ 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 wrapt==1.16.0 diff --git a/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py b/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py index 1e2a851e48..5fc59b542d 100644 --- a/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py +++ b/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py @@ -32,7 +32,9 @@ class TestLoad(TestCase): @patch( "opentelemetry.instrumentation.auto_instrumentation._load.iter_entry_points" ) - def test_load_configurators(self, iter_mock): + def test_load_configurators( + self, iter_mock + ): # pylint: disable=no-self-use # Add multiple entry points but only specify the 2nd in the environment variable. ep_mock1 = Mock() ep_mock1.name = "custom_configurator1" @@ -62,9 +64,8 @@ def test_load_configurators(self, iter_mock): "opentelemetry.instrumentation.auto_instrumentation._load.iter_entry_points" ) def test_load_configurators_no_ep( - self, - iter_mock, - ): + self, iter_mock + ): # pylint: disable=no-self-use iter_mock.return_value = () # Confirm method does not crash if not entry points exist. _load._load_configurators() @@ -214,6 +215,7 @@ def test_load_distro_error(self, iter_mock, isinstance_mock): ) def test_load_instrumentors(self, iter_mock, dep_mock): # Mock opentelemetry_pre_instrument entry points + # pylint: disable=too-many-locals pre_ep_mock1 = Mock() pre_ep_mock1.name = "pre1" pre_mock1 = Mock() @@ -285,7 +287,9 @@ def test_load_instrumentors(self, iter_mock, dep_mock): @patch( "opentelemetry.instrumentation.auto_instrumentation._load.iter_entry_points" ) - def test_load_instrumentors_dep_conflict(self, iter_mock, dep_mock): + def test_load_instrumentors_dep_conflict( + self, iter_mock, dep_mock + ): # pylint: disable=no-self-use ep_mock1 = Mock() ep_mock1.name = "instr1" diff --git a/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage/processor.py b/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage/processor.py index d14cf3a7e6..7e09e591e0 100644 --- a/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage/processor.py +++ b/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage/processor.py @@ -23,7 +23,7 @@ BaggageKeyPredicateT = Callable[[str], bool] # A BaggageKeyPredicate that always returns True, allowing all baggage keys to be added to spans -ALLOW_ALL_BAGGAGE_KEYS: BaggageKeyPredicateT = lambda _: True +ALLOW_ALL_BAGGAGE_KEYS: BaggageKeyPredicateT = lambda _: True # noqa: E731 class BaggageSpanProcessor(SpanProcessor): diff --git a/propagator/opentelemetry-propagator-aws-xray/benchmark-requirements.txt b/propagator/opentelemetry-propagator-aws-xray/benchmark-requirements.txt new file mode 100644 index 0000000000..44564857ef --- /dev/null +++ b/propagator/opentelemetry-propagator-aws-xray/benchmark-requirements.txt @@ -0,0 +1 @@ +pytest-benchmark==4.0.0 diff --git a/propagator/opentelemetry-propagator-aws-xray/tests/performance/benchmarks/test_benchmark_aws_xray_propagator.py b/propagator/opentelemetry-propagator-aws-xray/benchmarks/test_benchmark_aws_xray_propagator.py similarity index 100% rename from propagator/opentelemetry-propagator-aws-xray/tests/performance/benchmarks/test_benchmark_aws_xray_propagator.py rename to propagator/opentelemetry-propagator-aws-xray/benchmarks/test_benchmark_aws_xray_propagator.py diff --git a/propagator/opentelemetry-propagator-ot-trace/test-requirements.txt b/propagator/opentelemetry-propagator-ot-trace/test-requirements.txt index 59c30eabf4..a05528b67e 100644 --- a/propagator/opentelemetry-propagator-ot-trace/test-requirements.txt +++ b/propagator/opentelemetry-propagator-ot-trace/test-requirements.txt @@ -6,7 +6,6 @@ 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 wrapt==1.16.0 diff --git a/resource/opentelemetry-resource-detector-azure/pyproject.toml b/resource/opentelemetry-resource-detector-azure/pyproject.toml index f86f1f097b..fec99ef157 100644 --- a/resource/opentelemetry-resource-detector-azure/pyproject.toml +++ b/resource/opentelemetry-resource-detector-azure/pyproject.toml @@ -26,6 +26,7 @@ classifiers = [ ] dependencies = [ "opentelemetry-sdk ~= 1.21", + "opentelemetry-instrumentation == 0.47b0.dev", ] [project.entry-points.opentelemetry_resource_detector] diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py index 41371b8eec..7ca84a963d 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py @@ -12,23 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional from os import environ +from opentelemetry.resource.detector.azure._utils import ( + _get_azure_resource_uri, + _is_on_functions, +) from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, ResourceAttributes, ) -from opentelemetry.resource.detector.azure._utils import _get_azure_resource_uri - -from ._constants import ( - _APP_SERVICE_ATTRIBUTE_ENV_VARS, - _WEBSITE_SITE_NAME, -) -from opentelemetry.resource.detector.azure._utils import _is_on_functions +from ._constants import _APP_SERVICE_ATTRIBUTE_ENV_VARS, _WEBSITE_SITE_NAME class AzureAppServiceResourceDetector(ResourceDetector): diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/functions.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/functions.py index 0bf9a10f86..868c3cdd2b 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/functions.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/functions.py @@ -14,6 +14,10 @@ from os import environ, getpid +from opentelemetry.resource.detector.azure._utils import ( + _get_azure_resource_uri, + _is_on_functions, +) from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.semconv.resource import ( CloudPlatformValues, @@ -26,10 +30,6 @@ _REGION_NAME, _WEBSITE_SITE_NAME, ) -from opentelemetry.resource.detector.azure._utils import ( - _get_azure_resource_uri, - _is_on_functions, -) class AzureFunctionsResourceDetector(ResourceDetector): @@ -65,4 +65,3 @@ def detect(self) -> Resource: attributes[key] = value return Resource(attributes) - diff --git a/resource/opentelemetry-resource-detector-azure/test-requirements.txt b/resource/opentelemetry-resource-detector-azure/test-requirements.txt new file mode 100644 index 0000000000..13e58dc01f --- /dev/null +++ b/resource/opentelemetry-resource-detector-azure/test-requirements.txt @@ -0,0 +1,13 @@ +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 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e resource/opentelemetry-resource-detector-azure diff --git a/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py b/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py index 6c3d395994..4554440a38 100644 --- a/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py +++ b/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py @@ -68,7 +68,7 @@ def test_on_app_service(self): self.assertEqual( attributes["azure.app.service.stamp"], TEST_WEBSITE_HOME_STAMPNAME ) - + @patch.dict( "os.environ", { diff --git a/resource/opentelemetry-resource-detector-container/test-requirements.txt b/resource/opentelemetry-resource-detector-container/test-requirements.txt index ecacb62b12..07dd186f30 100644 --- a/resource/opentelemetry-resource-detector-container/test-requirements.txt +++ b/resource/opentelemetry-resource-detector-container/test-requirements.txt @@ -6,7 +6,6 @@ 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 wrapt==1.16.0 diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/benchmark-requirements.txt b/sdk-extension/opentelemetry-sdk-extension-aws/benchmark-requirements.txt new file mode 100644 index 0000000000..44564857ef --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/benchmark-requirements.txt @@ -0,0 +1 @@ +pytest-benchmark==4.0.0 diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/performance/benchmarks/trace/test_benchmark_aws_xray_ids_generator.py b/sdk-extension/opentelemetry-sdk-extension-aws/benchmarks/trace/test_benchmark_aws_xray_ids_generator.py similarity index 87% rename from sdk-extension/opentelemetry-sdk-extension-aws/tests/performance/benchmarks/trace/test_benchmark_aws_xray_ids_generator.py rename to sdk-extension/opentelemetry-sdk-extension-aws/benchmarks/trace/test_benchmark_aws_xray_ids_generator.py index c518755c60..573b3ee437 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/performance/benchmarks/trace/test_benchmark_aws_xray_ids_generator.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/benchmarks/trace/test_benchmark_aws_xray_ids_generator.py @@ -12,7 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from opentelemetry.sdk.extension.aws.trace import AwsXRayIdGenerator +from opentelemetry.sdk.extension.aws.trace import ( # pylint: disable=no-name-in-module + AwsXRayIdGenerator, +) id_generator = AwsXRayIdGenerator() diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test__lambda.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test__lambda.py index d23df95ebb..e183525e49 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test__lambda.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test__lambda.py @@ -16,7 +16,7 @@ from collections import OrderedDict from unittest.mock import patch -from opentelemetry.sdk.extension.aws.resource._lambda import ( +from opentelemetry.sdk.extension.aws.resource._lambda import ( # pylint: disable=no-name-in-module AwsLambdaResourceDetector, ) from opentelemetry.semconv.resource import ( diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_beanstalk.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_beanstalk.py index 08def19002..29db8e459c 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_beanstalk.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_beanstalk.py @@ -16,7 +16,7 @@ from collections import OrderedDict from unittest.mock import mock_open, patch -from opentelemetry.sdk.extension.aws.resource.beanstalk import ( +from opentelemetry.sdk.extension.aws.resource.beanstalk import ( # pylint: disable=no-name-in-module AwsBeanstalkResourceDetector, ) from opentelemetry.semconv.resource import ( diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_ec2.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_ec2.py index 530fb75b48..300f963ac5 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_ec2.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_ec2.py @@ -16,7 +16,9 @@ from collections import OrderedDict from unittest.mock import patch -from opentelemetry.sdk.extension.aws.resource.ec2 import AwsEc2ResourceDetector +from opentelemetry.sdk.extension.aws.resource.ec2 import ( # pylint: disable=no-name-in-module + AwsEc2ResourceDetector, +) from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_ecs.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_ecs.py index 0ef77ad49e..4f892cc1bb 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_ecs.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_ecs.py @@ -12,12 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os import unittest from collections import OrderedDict +from os.path import dirname, join from unittest.mock import mock_open, patch -from opentelemetry.sdk.extension.aws.resource.ecs import AwsEcsResourceDetector +from opentelemetry.sdk.extension.aws.resource.ecs import ( # pylint: disable=no-name-in-module + AwsEcsResourceDetector, +) from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, @@ -33,8 +35,10 @@ def _read_file(filename: str) -> str: - with open(os.path.join(os.path.dirname(__file__), "ecs", filename)) as f: - return f.read() + with open( + join(dirname(__file__), "ecs", filename), encoding="utf-8" + ) as file: + return file.read() MetadataV4Uri = "mock-uri-4" @@ -63,6 +67,7 @@ def _http_get_function_ec2(url: str, *args, **kwargs) -> str: return MetadataV4ContainerResponseEc2 if url == f"{MetadataV4Uri}/task": return MetadataV4TaskResponseEc2 + return None def _http_get_function_fargate(url: str, *args, **kwargs) -> str: @@ -70,6 +75,7 @@ def _http_get_function_fargate(url: str, *args, **kwargs) -> str: return MetadataV4ContainerResponseFargate if url == f"{MetadataV4Uri}/task": return MetadataV4TaskResponseFargate + return None class AwsEcsResourceDetectorTest(unittest.TestCase): @@ -150,7 +156,6 @@ def test_simple_create_metadata_v4_launchtype_ec2( ): mock_http_get_function.side_effect = _http_get_function_ec2 actual = AwsEcsResourceDetector().detect() - self.maxDiff = None self.assertDictEqual( actual.attributes.copy(), OrderedDict( @@ -215,7 +220,6 @@ def test_simple_create_metadata_v4_launchtype_fargate( ): mock_http_get_function.side_effect = _http_get_function_fargate actual = AwsEcsResourceDetector().detect() - self.maxDiff = None self.assertDictEqual( actual.attributes.copy(), OrderedDict( diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_eks.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_eks.py index 5edb9c5352..a3819e6383 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_eks.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_eks.py @@ -16,7 +16,9 @@ from collections import OrderedDict from unittest.mock import mock_open, patch -from opentelemetry.sdk.extension.aws.resource.eks import AwsEksResourceDetector +from opentelemetry.sdk.extension.aws.resource.eks import ( # pylint: disable=no-name-in-module + AwsEksResourceDetector, +) from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, diff --git a/tox.ini b/tox.ini index e18bdf6a6f..33f0242c5a 100644 --- a/tox.ini +++ b/tox.ini @@ -11,10 +11,16 @@ envlist = pypy3-test-resource-detector-container lint-resource-detector-container + ; opentelemetry-resource-detector-azure + py3{8,9,10,11,12}-test-resource-detector-azure + pypy3-test-resource-detector-azure + lint-resource-detector-azure + ; opentelemetry-sdk-extension-aws py3{8,9,10,11,12}-test-sdk-extension-aws pypy3-test-sdk-extension-aws lint-sdk-extension-aws + benchmark-sdk-extension-aws ; opentelemetry-distro py3{8,9,10,11,12}-test-distro @@ -303,11 +309,13 @@ envlist = ; opentelemetry-util-http py3{8,9,10,11,12}-test-util-http pypy3-test-util-http + lint-util-http ; opentelemetry-propagator-aws-xray py3{8,9,10,11,12}-test-propagator-aws-xray pypy3-test-propagator-aws-xray lint-propagator-aws-xray + benchmark-propagator-aws-xray ; opentelemetry-propagator-ot-trace py3{8,9,10,11,12}-test-propagator-ot-trace @@ -367,15 +375,13 @@ envlist = [testenv] deps = - -c dev-requirements.txt lint: -r dev-requirements.txt - test: pytest - test: pytest-benchmark coverage: pytest coverage: pytest-cov ; FIXME: add coverage testing ; FIXME: add mypy testing +allowlist_externals = sh setenv = ; override CORE_REPO_SHA via env variable when testing other branches/commits than main @@ -737,6 +743,7 @@ commands_pre = sdk-extension-aws: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk sdk-extension-aws: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils sdk-extension-aws: pip install -r {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/test-requirements.txt + benchmark-sdk-extension-aws: pip install -r {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/benchmark-requirements.txt resource-detector-container: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api resource-detector-container: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions @@ -744,6 +751,12 @@ commands_pre = resource-detector-container: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils resource-detector-container: pip install -r {toxinidir}/resource/opentelemetry-resource-detector-container/test-requirements.txt + resource-detector-azure: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + resource-detector-azure: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + resource-detector-azure: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + resource-detector-azure: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + resource-detector-azure: pip install -r {toxinidir}/resource/opentelemetry-resource-detector-azure/test-requirements.txt + propagator-ot-trace: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api propagator-ot-trace: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions propagator-ot-trace: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk @@ -755,18 +768,19 @@ commands_pre = propagator-aws-xray: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk propagator-aws-xray: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils propagator-aws-xray: pip install -r {toxinidir}/propagator/opentelemetry-propagator-aws-xray/test-requirements.txt + benchmark-propagator-aws-xray: pip install -r {toxinidir}/propagator/opentelemetry-propagator-aws-xray/benchmark-requirements.txt processor-baggage: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api processor-baggage: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions 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 + util-http: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + util-http: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + util-http: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + util-http: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + util-http: pip install -r {toxinidir}/util/opentelemetry-util-http/test-requirements.txt + util-http: pip install {toxinidir}/util/opentelemetry-util-http ; In order to get a health coverage report, ; we have to install packages in editable mode. @@ -777,402 +791,357 @@ commands = lint-distro: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/opentelemetry-distro lint-distro: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/opentelemetry-distro lint-distro: flake8 --config {toxinidir}/.flake8 {toxinidir}/opentelemetry-distro - lint-distro: pylint {toxinidir}/opentelemetry-distro/src/opentelemetry - lint-distro: pylint {toxinidir}/opentelemetry-distro/tests + lint-distro: pylint {toxinidir}/opentelemetry-distro test-opentelemetry-instrumentation: pytest {toxinidir}/opentelemetry-instrumentation/tests {posargs} lint-opentelemetry-instrumentation: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/opentelemetry-instrumentation lint-opentelemetry-instrumentation: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/opentelemetry-instrumentation lint-opentelemetry-instrumentation: flake8 --config {toxinidir}/.flake8 {toxinidir}/opentelemetry-instrumentation - lint-opentelemetry-instrumentation: pylint {toxinidir}/opentelemetry-instrumentation/src/opentelemetry - lint-opentelemetry-instrumentation: pylint {toxinidir}/opentelemetry-instrumentation/tests + lint-opentelemetry-instrumentation: pylint {toxinidir}/opentelemetry-instrumentation test-instrumentation-aiohttp-client: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests {posargs} lint-instrumentation-aiohttp-client: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client lint-instrumentation-aiohttp-client: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client lint-instrumentation-aiohttp-client: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client - lint-instrumentation-aiohttp-client: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry - lint-instrumentation-aiohttp-client: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests + lint-instrumentation-aiohttp-client: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-aiohttp-client" test-instrumentation-aiohttp-server: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests {posargs} lint-instrumentation-aiohttp-server: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server lint-instrumentation-aiohttp-server: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server lint-instrumentation-aiohttp-server: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server - lint-instrumentation-aiohttp-server: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry - lint-instrumentation-aiohttp-server: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests + lint-instrumentation-aiohttp-server: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-aiohttp-server" test-instrumentation-aiopg: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg/tests {posargs} lint-instrumentation-aiopg: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg lint-instrumentation-aiopg: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg lint-instrumentation-aiopg: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg - lint-instrumentation-aiopg: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry - lint-instrumentation-aiopg: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg/tests + lint-instrumentation-aiopg: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-aiopg" test-instrumentation-asgi: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi/tests {posargs} lint-instrumentation-asgi: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi lint-instrumentation-asgi: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi lint-instrumentation-asgi: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi - lint-instrumentation-asgi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry - lint-instrumentation-asgi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi/tests + lint-instrumentation-asgi: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-asgi" test-instrumentation-asyncpg: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg/tests {posargs} lint-instrumentation-asyncpg: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg lint-instrumentation-asyncpg: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg lint-instrumentation-asyncpg: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg - lint-instrumentation-asyncpg: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry - lint-instrumentation-asyncpg: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg/tests + lint-instrumentation-asyncpg: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-asyncpg" test-instrumentation-aws-lambda: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda/tests {posargs} lint-instrumentation-aws-lambda: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda lint-instrumentation-aws-lambda: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda lint-instrumentation-aws-lambda: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda - lint-instrumentation-aws-lambda: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry - lint-instrumentation-aws-lambda: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda/tests + lint-instrumentation-aws-lambda: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-aws-lambda" test-instrumentation-boto: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-boto/tests {posargs} lint-instrumentation-boto: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-boto lint-instrumentation-boto: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-boto lint-instrumentation-boto: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-boto - lint-instrumentation-boto: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry - lint-instrumentation-boto: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-boto/tests + lint-instrumentation-boto: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-boto" test-instrumentation-botocore: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/tests {posargs} lint-instrumentation-botocore: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore lint-instrumentation-botocore: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore lint-instrumentation-botocore: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore - lint-instrumentation-botocore: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry - lint-instrumentation-botocore: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/tests + lint-instrumentation-botocore: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-botocore" test-instrumentation-boto3sqs: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs/tests {posargs} lint-instrumentation-boto3sqs: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs lint-instrumentation-boto3sqs: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs lint-instrumentation-boto3sqs: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs - lint-instrumentation-boto3sqs: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry - lint-instrumentation-boto3sqs: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs/tests + lint-instrumentation-boto3sqs: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-boto3sqs" test-instrumentation-cassandra: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra/tests {posargs} lint-instrumentation-cassandra: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra lint-instrumentation-cassandra: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra lint-instrumentation-cassandra: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra - lint-instrumentation-cassandra: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra/src/opentelemetry - lint-instrumentation-cassandra: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra/tests + lint-instrumentation-cassandra: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-cassandra" test-instrumentation-celery: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/tests {posargs} lint-instrumentation-celery: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-celery lint-instrumentation-celery: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-celery lint-instrumentation-celery: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-celery - lint-instrumentation-celery: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry - lint-instrumentation-celery: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/tests + lint-instrumentation-celery: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-celery" test-instrumentation-dbapi: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi/tests {posargs} lint-instrumentation-dbapi: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi lint-instrumentation-dbapi: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi lint-instrumentation-dbapi: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi - lint-instrumentation-dbapi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry - lint-instrumentation-dbapi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi/tests + lint-instrumentation-dbapi: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-dbapi" test-instrumentation-django: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-django/tests {posargs} lint-instrumentation-django: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-django lint-instrumentation-django: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-django lint-instrumentation-django: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-django - lint-instrumentation-django: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry - lint-instrumentation-django: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-django/tests + lint-instrumentation-django: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-django" test-instrumentation-elasticsearch: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/tests {posargs} lint-instrumentation-elasticsearch: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch lint-instrumentation-elasticsearch: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch lint-instrumentation-elasticsearch: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch - lint-instrumentation-elasticsearch: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry - lint-instrumentation-elasticsearch: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/tests + lint-instrumentation-elasticsearch: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-elasticsearch" test-instrumentation-falcon: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/tests {posargs} lint-instrumentation-falcon: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon lint-instrumentation-falcon: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon lint-instrumentation-falcon: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon - lint-instrumentation-falcon: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry - lint-instrumentation-falcon: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/tests + lint-instrumentation-falcon: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-falcon" test-instrumentation-fastapi: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi/tests {posargs} lint-instrumentation-fastapi: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi lint-instrumentation-fastapi: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi lint-instrumentation-fastapi: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi - lint-instrumentation-fastapi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry - lint-instrumentation-fastapi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi/tests + lint-instrumentation-fastapi: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-fastapi" test-instrumentation-flask: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/tests {posargs} lint-instrumentation-flask: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-flask lint-instrumentation-flask: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-flask lint-instrumentation-flask: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-flask - lint-instrumentation-flask: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry - lint-instrumentation-flask: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/tests + lint-instrumentation-flask: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-flask" test-instrumentation-urllib: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib/tests {posargs} lint-instrumentation-urllib: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib lint-instrumentation-urllib: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib lint-instrumentation-urllib: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib - lint-instrumentation-urllib: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry - lint-instrumentation-urllib: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib/tests + lint-instrumentation-urllib: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-urllib" test-instrumentation-urllib3: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/tests {posargs} lint-instrumentation-urllib3: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3 lint-instrumentation-urllib3: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3 lint-instrumentation-urllib3: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3 - lint-instrumentation-urllib3: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry - lint-instrumentation-urllib3: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/tests + lint-instrumentation-urllib3: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-urllib3" test-instrumentation-grpc: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc/tests {posargs} lint-instrumentation-grpc: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc lint-instrumentation-grpc: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc lint-instrumentation-grpc: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc - lint-instrumentation-grpc: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry - lint-instrumentation-grpc: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc/tests + lint-instrumentation-grpc: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-grpc" test-instrumentation-jinja2: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2/tests {posargs} lint-instrumentation-jinja2: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2 lint-instrumentation-jinja2: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2 lint-instrumentation-jinja2: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2 - lint-instrumentation-jinja2: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry - lint-instrumentation-jinja2: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2/tests + lint-instrumentation-jinja2: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-jinja2" test-instrumentation-kafka-python: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python/tests {posargs} lint-instrumentation-kafka-python: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python lint-instrumentation-kafka-python: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python lint-instrumentation-kafka-python: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python - lint-instrumentation-kafka-python: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry - lint-instrumentation-kafka-python: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python/tests + lint-instrumentation-kafka-python: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-kafka-python" test-instrumentation-confluent-kafka: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests {posargs} lint-instrumentation-confluent-kafka: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka lint-instrumentation-confluent-kafka: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka lint-instrumentation-confluent-kafka: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka - lint-instrumentation-confluent-kafka: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry - lint-instrumentation-confluent-kafka: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests + lint-instrumentation-confluent-kafka: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-confluent-kafka" test-instrumentation-logging: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-logging/tests {posargs} lint-instrumentation-logging: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-logging lint-instrumentation-logging: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-logging lint-instrumentation-logging: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-logging - lint-instrumentation-logging: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry - lint-instrumentation-logging: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-logging/tests + lint-instrumentation-logging: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-logging" test-instrumentation-mysql: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql/tests {posargs} lint-instrumentation-mysql: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql lint-instrumentation-mysql: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql lint-instrumentation-mysql: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql - lint-instrumentation-mysql: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry - lint-instrumentation-mysql: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql/tests + lint-instrumentation-mysql: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-mysql" test-instrumentation-mysqlclient: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient/tests {posargs} lint-instrumentation-mysqlclient: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient lint-instrumentation-mysqlclient: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient lint-instrumentation-mysqlclient: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient - lint-instrumentation-mysqlclient: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry - lint-instrumentation-mysqlclient: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient/tests + lint-instrumentation-mysqliclient: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-mysqliclient" test-instrumentation-sio-pika: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/tests {posargs} lint-instrumentation-sio-pika: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-pika lint-instrumentation-sio-pika: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-pika lint-instrumentation-sio-pika: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-pika - lint-instrumentation-sio-pika: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry - lint-instrumentation-sio-pika: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/tests + lint-instrumentation-sio-pika: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-pika" test-instrumentation-aio-pika: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/tests {posargs} lint-instrumentation-aio-pika: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika lint-instrumentation-aio-pika: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika lint-instrumentation-aio-pika: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika - lint-instrumentation-aio-pika: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry - lint-instrumentation-aio-pika: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/tests + lint-instrumentation-aio-pika: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-aio-pika" test-instrumentation-psycopg: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg/tests {posargs} lint-instrumentation-psycopg: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg lint-instrumentation-psycopg: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg lint-instrumentation-psycopg: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg - lint-instrumentation-psycopg: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry - lint-instrumentation-psycopg: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg/tests + lint-instrumentation-psycopg: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-psycopg" test-instrumentation-psycopg2: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2/tests {posargs} lint-instrumentation-psycopg2: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2 lint-instrumentation-psycopg2: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2 lint-instrumentation-psycopg2: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2 - lint-instrumentation-psycopg2: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry - lint-instrumentation-psycopg2: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2/tests + lint-instrumentation-psycopg2: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-psycopg2" test-instrumentation-pymemcache: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/tests {posargs} lint-instrumentation-pymemcache: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache lint-instrumentation-pymemcache: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache lint-instrumentation-pymemcache: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache - lint-instrumentation-pymemcache: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry - lint-instrumentation-pymemcache: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/tests + lint-instrumentation-pymemcache: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-pymemcache" test-instrumentation-pymongo: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo/tests {posargs} lint-instrumentation-pymongo: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo lint-instrumentation-pymongo: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo lint-instrumentation-pymongo: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo - lint-instrumentation-pymongo: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry - lint-instrumentation-pymongo: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo/tests + lint-instrumentation-pymongo: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-pymongo" test-instrumentation-pymysql: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql/tests {posargs} lint-instrumentation-pymysql: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql lint-instrumentation-pymysql: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql lint-instrumentation-pymysql: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql - lint-instrumentation-pymysql: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry - lint-instrumentation-pymysql: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql/tests + lint-instrumentation-pymysql: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-pymysql" test-instrumentation-pyramid: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid/tests {posargs} lint-instrumentation-pyramid: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid lint-instrumentation-pyramid: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid lint-instrumentation-pyramid: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid - lint-instrumentation-pyramid: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry - lint-instrumentation-pyramid: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid/tests + lint-instrumentation-pyramid: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-pyramid" test-instrumentation-redis: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-redis/tests {posargs} lint-instrumentation-redis: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-redis lint-instrumentation-redis: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-redis lint-instrumentation-redis: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-redis - lint-instrumentation-redis: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry - lint-instrumentation-redis: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-redis/tests + lint-instrumentation-redis: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-redis" test-instrumentation-remoulade: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade/tests {posargs} lint-instrumentation-remoulade: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade lint-instrumentation-remoulade: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade lint-instrumentation-remoulade: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade - lint-instrumentation-remoulade: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry - lint-instrumentation-remoulade: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade/tests + lint-instrumentation-remoulade: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-remoulade" test-instrumentation-requests: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-requests/tests {posargs} lint-instrumentation-requests: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-requests lint-instrumentation-requests: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-requests lint-instrumentation-requests: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-requests - lint-instrumentation-requests: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry - lint-instrumentation-requests: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-requests/tests + lint-instrumentation-requests: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-requests" test-instrumentation-sklearn: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn/tests {posargs} lint-instrumentation-sklearn: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn lint-instrumentation-sklearn: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn lint-instrumentation-sklearn: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn - lint-instrumentation-sklearn: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn/src/opentelemetry - lint-instrumentation-sklearn: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn/tests + lint-instrumentation-sklearn: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-sklearn" test-instrumentation-sqlalchemy: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests {posargs} lint-instrumentation-sqlalchemy: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy lint-instrumentation-sqlalchemy: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy lint-instrumentation-sqlalchemy: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy - lint-instrumentation-sqlalchemy: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry - lint-instrumentation-sqlalchemy: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests + lint-instrumentation-sqlalchemy: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-sqlalchemy" test-instrumentation-sqlite3: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3/tests {posargs} lint-instrumentation-sqlite3: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3 lint-instrumentation-sqlite3: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3 lint-instrumentation-sqlite3: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3 - lint-instrumentation-sqlite3: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry - lint-instrumentation-sqlite3: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3/tests + lint-instrumentation-sqlite3: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-sqlite3" test-instrumentation-starlette: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette/tests {posargs} lint-instrumentation-starlette: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette lint-instrumentation-starlette: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette lint-instrumentation-starlette: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette - lint-instrumentation-starlette: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry - lint-instrumentation-starlette: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette/tests + lint-instrumentation-starlette: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-starlette" test-instrumentation-system-metrics: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics/tests {posargs} lint-instrumentation-system-metrics: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics lint-instrumentation-system-metrics: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics lint-instrumentation-system-metrics: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics - lint-instrumentation-system-metrics: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry - lint-instrumentation-system-metrics: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics/tests + lint-instrumentation-system-metrics: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-system-metrics" test-instrumentation-threading: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-threading/tests {posargs} lint-instrumentation-threading: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-threading lint-instrumentation-threading: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-threading lint-instrumentation-threading: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-threading - lint-instrumentation-threading: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry - lint-instrumentation-threading: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-threading/tests + lint-instrumentation-threading: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-threading" test-instrumentation-tornado: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado/tests {posargs} lint-instrumentation-tornado: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado lint-instrumentation-tornado: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado lint-instrumentation-tornado: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado - lint-instrumentation-tornado: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry - lint-instrumentation-tornado: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado/tests + lint-instrumentation-tornado: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-tornado" test-instrumentation-tortoiseorm: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm/tests {posargs} lint-instrumentation-tortoiseorm: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm lint-instrumentation-tortoiseorm: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm lint-instrumentation-tortoiseorm: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm - lint-instrumentation-tortoiseorm: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry - lint-instrumentation-tortoiseorm: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm/tests + lint-instrumentation-tortoiseorm: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-tortoiseorm" test-instrumentation-wsgi: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi/tests {posargs} lint-instrumentation-wsgi: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi lint-instrumentation-wsgi: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi lint-instrumentation-wsgi: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi - lint-instrumentation-wsgi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry - lint-instrumentation-wsgi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi/tests + lint-instrumentation-wsgi: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-wsgi" test-instrumentation-httpx: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/tests {posargs} lint-instrumentation-httpx: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx lint-instrumentation-httpx: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx lint-instrumentation-httpx: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx - lint-instrumentation-httpx: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry - lint-instrumentation-httpx: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/tests + lint-instrumentation-httpx: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-httpx" test-instrumentation-asyncio: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio/tests {posargs} lint-instrumentation-asyncio: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio lint-instrumentation-asyncio: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio lint-instrumentation-asyncio: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio - lint-instrumentation-asyncio: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry - lint-instrumentation-asyncio: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio/tests + lint-instrumentation-asyncio: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-asyncio" test-util-http: pytest {toxinidir}/util/opentelemetry-util-http/tests {posargs} + lint-util-http: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/util/opentelemetry-util-http + lint-util-http: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/util/opentelemetry-util-http + lint-util-http: flake8 --config {toxinidir}/.flake8 {toxinidir}/util/opentelemetry-util-http + lint-util-http: sh -c "cd util && pylint --rcfile ../.pylintrc opentelemetry-util-http" test-sdk-extension-aws: pytest {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/tests {posargs} lint-sdk-extension-aws: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws lint-sdk-extension-aws: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws lint-sdk-extension-aws: flake8 --config {toxinidir}/.flake8 {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws - lint-sdk-extension-aws: pylint {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry - lint-sdk-extension-aws: pylint {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/tests + lint-sdk-extension-aws: sh -c "cd sdk-extension && pylint --rcfile ../.pylintrc opentelemetry-sdk-extension-aws" + benchmark-sdk-extension-aws: pytest {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/benchmarks {posargs} --benchmark-json=sdk-extension-aws-benchmark.json test-resource-detector-container: pytest {toxinidir}/resource/opentelemetry-resource-detector-container/tests {posargs} lint-resource-detector-container: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/resource/opentelemetry-resource-detector-container lint-resource-detector-container: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/resource/opentelemetry-resource-detector-container lint-resource-detector-container: flake8 --config {toxinidir}/.flake8 {toxinidir}/resource/opentelemetry-resource-detector-container - lint-resource-detector-container: pylint {toxinidir}/resource/opentelemetry-resource-detector-container/src/opentelemetry - lint-resource-detector-container: pylint {toxinidir}/resource/opentelemetry-resource-detector-container/tests + lint-resource-detector-container: sh -c "cd resource && pylint --rcfile ../.pylintrc opentelemetry-resource-detector-container" + + test-resource-detector-azure: pytest {toxinidir}/resource/opentelemetry-resource-detector-azure/tests {posargs} + lint-resource-detector-azure: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/resource/opentelemetry-resource-detector-azure + lint-resource-detector-azure: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/resource/opentelemetry-resource-detector-azure + lint-resource-detector-azure: flake8 --config {toxinidir}/.flake8 {toxinidir}/resource/opentelemetry-resource-detector-azure + lint-resource-detector-azure: sh -c "cd resource && pylint --rcfile ../.pylintrc opentelemetry-resource-detector-azure" test-processor-baggage: pytest {toxinidir}/processor/opentelemetry-processor-baggage/tests {posargs} lint-processor-baggage: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/processor/opentelemetry-processor-baggage lint-processor-baggage: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/processor/opentelemetry-processor-baggage lint-processor-baggage: flake8 --config {toxinidir}/.flake8 {toxinidir}/processor/opentelemetry-processor-baggage - lint-processor-baggage: pylint {toxinidir}/processor/opentelemetry-processor-baggage/src/opentelemetry - lint-processor-baggage: pylint {toxinidir}/processor/opentelemetry-processor-baggage/tests + lint-processor-baggage: sh -c "cd processor && pylint --rcfile ../.pylintrc opentelemetry-processor-baggage" test-propagator-aws-xray: pytest {toxinidir}/propagator/opentelemetry-propagator-aws-xray/tests {posargs} lint-propagator-aws-xray: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/propagator/opentelemetry-propagator-aws-xray lint-propagator-aws-xray: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/propagator/opentelemetry-propagator-aws-xray lint-propagator-aws-xray: flake8 --config {toxinidir}/.flake8 {toxinidir}/propagator/opentelemetry-propagator-aws-xray - lint-propagator-aws-xray: pylint {toxinidir}/propagator/opentelemetry-propagator-aws-xray/src/opentelemetry - lint-propagator-aws-xray: pylint {toxinidir}/propagator/opentelemetry-propagator-aws-xray/tests + lint-propagator-aws-xray: sh -c "cd propagator && pylint --rcfile ../.pylintrc opentelemetry-propagator-aws-xray" + benchmark-propagator-aws-xray: pytest {toxinidir}/propagator/opentelemetry-propagator-aws-xray/benchmarks {posargs} --benchmark-json=propagator-aws-xray-benchmark.json test-propagator-ot-trace: pytest {toxinidir}/propagator/opentelemetry-propagator-ot-trace/tests {posargs} lint-propagator-ot-trace: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/propagator/opentelemetry-propagator-ot-trace lint-propagator-ot-trace: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/propagator/opentelemetry-propagator-ot-trace lint-propagator-ot-trace: flake8 --config {toxinidir}/.flake8 {toxinidir}/propagator/opentelemetry-propagator-ot-trace - lint-propagator-ot-trace: pylint {toxinidir}/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry - lint-propagator-ot-trace: pylint {toxinidir}/propagator/opentelemetry-propagator-ot-trace/tests + lint-propagator-ot-trace: sh -c "cd propagator && pylint --rcfile ../.pylintrc opentelemetry-propagator-ot-trace" test-exporter-richconsole: pytest {toxinidir}/exporter/opentelemetry-exporter-richconsole/tests {posargs} lint-exporter-richconsole: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/exporter/opentelemetry-exporter-richconsole lint-exporter-richconsole: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/exporter/opentelemetry-exporter-richconsole lint-exporter-richconsole: flake8 --config {toxinidir}/.flake8 {toxinidir}/exporter/opentelemetry-exporter-richconsole - lint-exporter-richconsole: pylint {toxinidir}/exporter/opentelemetry-exporter-richconsole/src/opentelemetry - lint-exporter-richconsole: pylint {toxinidir}/exporter/opentelemetry-exporter-richconsole/tests + lint-exporter-richconsole: sh -c "cd exporter && pylint --rcfile ../.pylintrc opentelemetry-exporter-richconsole" test-exporter-prometheus-remote-write: pytest {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write/tests {posargs} lint-exporter-prometheus-remote-write: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write lint-exporter-prometheus-remote-write: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write lint-exporter-prometheus-remote-write: flake8 --config {toxinidir}/.flake8 {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write - lint-exporter-prometheus-remote-write: pylint {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry - lint-exporter-prometheus-remote-write: pylint {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write/tests + lint-exporter-prometheus-remote-write: sh -c "cd exporter && pylint --rcfile ../.pylintrc opentelemetry-exporter-prometheus-remote-write" coverage: {toxinidir}/scripts/coverage.sh diff --git a/util/opentelemetry-util-http/test-requirements.txt b/util/opentelemetry-util-http/test-requirements.txt index 0e28bbdd05..cb1ffe135b 100644 --- a/util/opentelemetry-util-http/test-requirements.txt +++ b/util/opentelemetry-util-http/test-requirements.txt @@ -6,7 +6,6 @@ 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_remove_credentials.py b/util/opentelemetry-util-http/tests/test_remove_credentials.py index b6243145f5..2e50f1495f 100644 --- a/util/opentelemetry-util-http/tests/test_remove_credentials.py +++ b/util/opentelemetry-util-http/tests/test_remove_credentials.py @@ -19,9 +19,13 @@ def test_remove_credentials(self): def test_remove_credentials_ipv4_literal(self): url = "http://someuser:somepass@127.0.0.1:8080/test/path?query=value" cleaned_url = remove_url_credentials(url) - self.assertEqual(cleaned_url, "http://127.0.0.1:8080/test/path?query=value") + self.assertEqual( + cleaned_url, "http://127.0.0.1:8080/test/path?query=value" + ) def test_remove_credentials_ipv6_literal(self): url = "http://someuser:somepass@[::1]:8080/test/path?query=value" cleaned_url = remove_url_credentials(url) - self.assertEqual(cleaned_url, "http://[::1]:8080/test/path?query=value") + self.assertEqual( + cleaned_url, "http://[::1]:8080/test/path?query=value" + )