diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f0cfeb4ad..fd6d774f59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -## Added +### Added - `opentelemetry-instrumentation-kafka-python` Instrument temporary fork, kafka-python-ng inside kafka-python's instrumentation @@ -15,17 +15,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-fastapi` Add ability to disable internal HTTP send and receive spans ([#2802](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2802)) -## Breaking changes +### Breaking changes - `opentelemetry-bootstrap` Remove `opentelemetry-instrumentation-aws-lambda` from the defaults instrumentations ([#2786](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2786)) -## Fixed +### Fixed +- `opentelemetry-instrumentation-httpx` fix handling of async hooks + ([#2823](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2823)) - `opentelemetry-instrumentation-system-metrics` fix `process.runtime.cpu.utilization` values to be shown in range of 0 to 1 - ([2812](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2812)) + ([#2812](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2812)) - `opentelemetry-instrumentation-fastapi` fix `fastapi` auto-instrumentation by removing `fastapi-slim` support, `fastapi-slim` itself is discontinued from maintainers - ([2783](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2783)) + ([#2783](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2783)) - `opentelemetry-instrumentation-aws-lambda` Avoid exception when a handler is not present. ([#2750](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2750)) - `opentelemetry-instrumentation-django` Fix regression - `http.target` re-added back to old semconv duration metrics diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py index 5394d62ff0..f6cb05fbda 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py @@ -1,7 +1,21 @@ +# 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 os import opentelemetry.instrumentation.asgi as otel_asgi -from opentelemetry.test.asgitestutil import AsgiTestBase +from opentelemetry.test.asgitestutil import AsyncAsgiTestBase from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind from opentelemetry.util.http import ( @@ -90,7 +104,7 @@ async def websocket_app_with_custom_headers(scope, receive, send): break -class TestCustomHeaders(AsgiTestBase, TestBase): +class TestCustomHeaders(AsyncAsgiTestBase): constructor_params = {} __test__ = False @@ -108,7 +122,7 @@ def setUp(self): **self.constructor_params, ) - def test_http_custom_request_headers_in_span_attributes(self): + async def test_http_custom_request_headers_in_span_attributes(self): self.scope["headers"].extend( [ (b"custom-test-header-1", b"test-header-value-1"), @@ -119,8 +133,8 @@ def test_http_custom_request_headers_in_span_attributes(self): ] ) self.seed_app(self.app) - self.send_default_request() - self.get_all_output() + await self.send_default_request() + await self.get_all_output() span_list = self.exporter.get_finished_spans() expected = { "http.request.header.custom_test_header_1": ( @@ -139,7 +153,7 @@ def test_http_custom_request_headers_in_span_attributes(self): if span.kind == SpanKind.SERVER: self.assertSpanHasAttributes(span, expected) - def test_http_repeat_request_headers_in_span_attributes(self): + async def test_http_repeat_request_headers_in_span_attributes(self): self.scope["headers"].extend( [ (b"custom-test-header-1", b"test-header-value-1"), @@ -147,8 +161,8 @@ def test_http_repeat_request_headers_in_span_attributes(self): ] ) self.seed_app(self.app) - self.send_default_request() - self.get_all_output() + await self.send_default_request() + await self.get_all_output() span_list = self.exporter.get_finished_spans() expected = { "http.request.header.custom_test_header_1": ( @@ -159,15 +173,15 @@ def test_http_repeat_request_headers_in_span_attributes(self): span = next(span for span in span_list if span.kind == SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) - def test_http_custom_request_headers_not_in_span_attributes(self): + async def test_http_custom_request_headers_not_in_span_attributes(self): self.scope["headers"].extend( [ (b"custom-test-header-1", b"test-header-value-1"), ] ) self.seed_app(self.app) - self.send_default_request() - self.get_all_output() + await self.send_default_request() + await self.get_all_output() span_list = self.exporter.get_finished_spans() expected = { "http.request.header.custom_test_header_1": ( @@ -185,15 +199,15 @@ def test_http_custom_request_headers_not_in_span_attributes(self): for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) - def test_http_custom_response_headers_in_span_attributes(self): + async def test_http_custom_response_headers_in_span_attributes(self): self.app = otel_asgi.OpenTelemetryMiddleware( http_app_with_custom_headers, tracer_provider=self.tracer_provider, **self.constructor_params, ) self.seed_app(self.app) - self.send_default_request() - self.get_all_output() + await self.send_default_request() + await self.get_all_output() span_list = self.exporter.get_finished_spans() expected = { "http.response.header.custom_test_header_1": ( @@ -214,15 +228,15 @@ def test_http_custom_response_headers_in_span_attributes(self): if span.kind == SpanKind.SERVER: self.assertSpanHasAttributes(span, expected) - def test_http_repeat_response_headers_in_span_attributes(self): + async def test_http_repeat_response_headers_in_span_attributes(self): self.app = otel_asgi.OpenTelemetryMiddleware( http_app_with_repeat_headers, tracer_provider=self.tracer_provider, **self.constructor_params, ) self.seed_app(self.app) - self.send_default_request() - self.get_all_output() + await self.send_default_request() + await self.get_all_output() span_list = self.exporter.get_finished_spans() expected = { "http.response.header.custom_test_header_1": ( @@ -233,15 +247,15 @@ def test_http_repeat_response_headers_in_span_attributes(self): span = next(span for span in span_list if span.kind == SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) - def test_http_custom_response_headers_not_in_span_attributes(self): + async def test_http_custom_response_headers_not_in_span_attributes(self): self.app = otel_asgi.OpenTelemetryMiddleware( http_app_with_custom_headers, tracer_provider=self.tracer_provider, **self.constructor_params, ) self.seed_app(self.app) - self.send_default_request() - self.get_all_output() + await self.send_default_request() + await self.get_all_output() span_list = self.exporter.get_finished_spans() not_expected = { "http.response.header.custom_test_header_3": ( @@ -253,7 +267,7 @@ def test_http_custom_response_headers_not_in_span_attributes(self): for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) - def test_websocket_custom_request_headers_in_span_attributes(self): + async def test_websocket_custom_request_headers_in_span_attributes(self): self.scope = { "type": "websocket", "http_version": "1.1", @@ -271,11 +285,11 @@ def test_websocket_custom_request_headers_in_span_attributes(self): "server": ("127.0.0.1", 80), } self.seed_app(self.app) - self.send_input({"type": "websocket.connect"}) - self.send_input({"type": "websocket.receive", "text": "ping"}) - self.send_input({"type": "websocket.disconnect"}) + await self.send_input({"type": "websocket.connect"}) + await self.send_input({"type": "websocket.receive", "text": "ping"}) + await self.send_input({"type": "websocket.disconnect"}) - self.get_all_output() + await self.get_all_output() span_list = self.exporter.get_finished_spans() expected = { "http.request.header.custom_test_header_1": ( @@ -294,7 +308,9 @@ def test_websocket_custom_request_headers_in_span_attributes(self): if span.kind == SpanKind.SERVER: self.assertSpanHasAttributes(span, expected) - def test_websocket_custom_request_headers_not_in_span_attributes(self): + async def test_websocket_custom_request_headers_not_in_span_attributes( + self, + ): self.scope = { "type": "websocket", "http_version": "1.1", @@ -309,11 +325,11 @@ def test_websocket_custom_request_headers_not_in_span_attributes(self): "server": ("127.0.0.1", 80), } self.seed_app(self.app) - self.send_input({"type": "websocket.connect"}) - self.send_input({"type": "websocket.receive", "text": "ping"}) - self.send_input({"type": "websocket.disconnect"}) + await self.send_input({"type": "websocket.connect"}) + await self.send_input({"type": "websocket.receive", "text": "ping"}) + await self.send_input({"type": "websocket.disconnect"}) - self.get_all_output() + await self.get_all_output() span_list = self.exporter.get_finished_spans() not_expected = { "http.request.header.custom_test_header_3": ( @@ -325,7 +341,7 @@ def test_websocket_custom_request_headers_not_in_span_attributes(self): for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) - def test_websocket_custom_response_headers_in_span_attributes(self): + async def test_websocket_custom_response_headers_in_span_attributes(self): self.scope = { "type": "websocket", "http_version": "1.1", @@ -342,10 +358,10 @@ def test_websocket_custom_response_headers_in_span_attributes(self): **self.constructor_params, ) self.seed_app(self.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() + await self.send_input({"type": "websocket.connect"}) + await self.send_input({"type": "websocket.receive", "text": "ping"}) + await self.send_input({"type": "websocket.disconnect"}) + await self.get_all_output() span_list = self.exporter.get_finished_spans() expected = { "http.response.header.custom_test_header_1": ( @@ -366,7 +382,9 @@ def test_websocket_custom_response_headers_in_span_attributes(self): if span.kind == SpanKind.SERVER: self.assertSpanHasAttributes(span, expected) - def test_websocket_custom_response_headers_not_in_span_attributes(self): + async def test_websocket_custom_response_headers_not_in_span_attributes( + self, + ): self.scope = { "type": "websocket", "http_version": "1.1", @@ -383,10 +401,10 @@ def test_websocket_custom_response_headers_not_in_span_attributes(self): **self.constructor_params, ) self.seed_app(self.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() + await self.send_input({"type": "websocket.connect"}) + await self.send_input({"type": "websocket.receive", "text": "ping"}) + await self.send_input({"type": "websocket.disconnect"}) + await self.get_all_output() span_list = self.exporter.get_finished_spans() not_expected = { "http.response.header.custom_test_header_3": ( diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py index 94e2e5e323..baa75bb329 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py @@ -14,7 +14,6 @@ # pylint: disable=too-many-lines -import asyncio import sys import time import unittest @@ -64,7 +63,7 @@ ) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.asgitestutil import ( - AsgiTestBase, + AsyncAsgiTestBase, setup_testing_defaults, ) from opentelemetry.test.test_base import TestBase @@ -275,7 +274,7 @@ async def error_asgi(scope, receive, send): # pylint: disable=too-many-public-methods -class TestAsgiApplication(AsgiTestBase): +class TestAsgiApplication(AsyncAsgiTestBase): def setUp(self): super().setUp() @@ -477,31 +476,31 @@ def validate_outputs( "opentelemetry.instrumentation.asgi", ) - def test_basic_asgi_call(self): + async def test_basic_asgi_call(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() + await self.send_default_request() + outputs = await self.get_all_output() self.validate_outputs(outputs) - def test_basic_asgi_call_new_semconv(self): + async 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() + await self.send_default_request() + outputs = await self.get_all_output() self.validate_outputs(outputs, old_sem_conv=False, new_sem_conv=True) - def test_basic_asgi_call_both_semconv(self): + async 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() + await self.send_default_request() + outputs = await self.get_all_output() self.validate_outputs(outputs, old_sem_conv=True, new_sem_conv=True) - def test_asgi_not_recording(self): + async def test_asgi_not_recording(self): mock_tracer = mock.Mock() mock_span = mock.Mock() mock_span.is_recording.return_value = False @@ -514,21 +513,21 @@ def test_asgi_not_recording(self): tracer.return_value = mock_tracer app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) - self.send_default_request() + await self.send_default_request() 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) - def test_asgi_exc_info(self): + async def test_asgi_exc_info(self): """Test that exception information is emitted as expected.""" app = otel_asgi.OpenTelemetryMiddleware(error_asgi) self.seed_app(app) - self.send_default_request() - outputs = self.get_all_output() + await self.send_default_request() + outputs = await self.get_all_output() self.validate_outputs(outputs, error=ValueError) - def test_long_response(self): + async def test_long_response(self): """Test that the server span is ended on the final response body message. If the server span is ended early then this test will fail due @@ -536,8 +535,8 @@ def test_long_response(self): """ app = otel_asgi.OpenTelemetryMiddleware(long_response_asgi) self.seed_app(app) - self.send_default_request() - outputs = self.get_all_output() + await self.send_default_request() + outputs = await self.get_all_output() def add_more_body_spans(expected: list): more_body_span = { @@ -551,12 +550,12 @@ def add_more_body_spans(expected: list): self.validate_outputs(outputs, modifiers=[add_more_body_spans]) - def test_background_execution(self): + async def test_background_execution(self): """Test that the server span is ended BEFORE the background task is finished.""" app = otel_asgi.OpenTelemetryMiddleware(background_execution_asgi) self.seed_app(app) - self.send_default_request() - outputs = self.get_all_output() + await self.send_default_request() + outputs = await self.get_all_output() self.validate_outputs(outputs) span_list = self.memory_exporter.get_finished_spans() server_span = span_list[-1] @@ -589,15 +588,15 @@ def test_exclude_internal_spans(self): for excluded_span in excluded_spans: self.assertNotEqual(span.name, excluded_span) - def test_trailers(self): + async def test_trailers(self): """Test that trailers are emitted as expected and that the server span is ended BEFORE the background task is finished.""" app = otel_asgi.OpenTelemetryMiddleware( background_execution_trailers_asgi ) self.seed_app(app) - self.send_default_request() - outputs = self.get_all_output() + await self.send_default_request() + outputs = await self.get_all_output() def add_body_and_trailer_span(expected: list): body_span = { @@ -624,7 +623,7 @@ def add_body_and_trailer_span(expected: list): _SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S * 10**9, ) - def test_override_span_name(self): + async def test_override_span_name(self): """Test that default span_names can be overwritten by our callback function.""" span_name = "Dymaxion" @@ -645,11 +644,11 @@ def update_expected_span_name(expected): simple_asgi, default_span_details=get_predefined_span_details ) self.seed_app(app) - self.send_default_request() - outputs = self.get_all_output() + await self.send_default_request() + outputs = await self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_span_name]) - def test_custom_tracer_provider_otel_asgi(self): + async def test_custom_tracer_provider_otel_asgi(self): resource = resources.Resource.create({"service-test-key": "value"}) result = TestBase.create_tracer_provider(resource=resource) tracer_provider, exporter = result @@ -658,28 +657,28 @@ def test_custom_tracer_provider_otel_asgi(self): simple_asgi, tracer_provider=tracer_provider ) self.seed_app(app) - self.send_default_request() + await self.send_default_request() span_list = exporter.get_finished_spans() for span in span_list: self.assertEqual( span.resource.attributes["service-test-key"], "value" ) - def test_no_op_tracer_provider_otel_asgi(self): + async def test_no_op_tracer_provider_otel_asgi(self): app = otel_asgi.OpenTelemetryMiddleware( simple_asgi, tracer_provider=trace_api.NoOpTracerProvider() ) self.seed_app(app) - self.send_default_request() + await self.send_default_request() - response_start, response_body, *_ = self.get_all_output() + response_start, response_body, *_ = await self.get_all_output() self.assertEqual(response_body["body"], b"*") self.assertEqual(response_start["status"], 200) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) - def test_behavior_with_scope_server_as_none(self): + async def test_behavior_with_scope_server_as_none(self): """Test that middleware is ok when server is none in scope.""" def update_expected_server(expected): @@ -695,11 +694,11 @@ def update_expected_server(expected): self.scope["server"] = None app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) - self.send_default_request() - outputs = self.get_all_output() + await self.send_default_request() + outputs = await self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_server]) - def test_behavior_with_scope_server_as_none_new_semconv(self): + async 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): @@ -714,8 +713,8 @@ def update_expected_server(expected): self.scope["server"] = None app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) - self.send_default_request() - outputs = self.get_all_output() + await self.send_default_request() + outputs = await self.get_all_output() self.validate_outputs( outputs, modifiers=[update_expected_server], @@ -723,7 +722,7 @@ def update_expected_server(expected): new_sem_conv=True, ) - def test_behavior_with_scope_server_as_none_both_semconv(self): + async 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): @@ -741,8 +740,8 @@ def update_expected_server(expected): self.scope["server"] = None app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) - self.send_default_request() - outputs = self.get_all_output() + await self.send_default_request() + outputs = await self.get_all_output() self.validate_outputs( outputs, modifiers=[update_expected_server], @@ -750,7 +749,7 @@ def update_expected_server(expected): new_sem_conv=True, ) - def test_host_header(self): + async def test_host_header(self): """Test that host header is converted to http.server_name.""" hostname = b"server_name_1" @@ -763,11 +762,11 @@ def update_expected_server(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() + await self.send_default_request() + outputs = await self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_server]) - def test_host_header_both_semconv(self): + async def test_host_header_both_semconv(self): """Test that host header is converted to http.server_name.""" hostname = b"server_name_1" @@ -780,8 +779,8 @@ def update_expected_server(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() + await self.send_default_request() + outputs = await self.get_all_output() self.validate_outputs( outputs, modifiers=[update_expected_server], @@ -789,7 +788,7 @@ def update_expected_server(expected): new_sem_conv=True, ) - def test_user_agent(self): + async def test_user_agent(self): """Test that host header is converted to http.server_name.""" user_agent = b"test-agent" @@ -802,11 +801,11 @@ def update_expected_user_agent(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() + await self.send_default_request() + outputs = await self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_user_agent]) - def test_user_agent_new_semconv(self): + async def test_user_agent_new_semconv(self): """Test that host header is converted to http.server_name.""" user_agent = b"test-agent" @@ -819,8 +818,8 @@ def update_expected_user_agent(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() + await self.send_default_request() + outputs = await self.get_all_output() self.validate_outputs( outputs, modifiers=[update_expected_user_agent], @@ -828,7 +827,7 @@ def update_expected_user_agent(expected): new_sem_conv=True, ) - def test_user_agent_both_semconv(self): + async def test_user_agent_both_semconv(self): """Test that host header is converted to http.server_name.""" user_agent = b"test-agent" @@ -844,8 +843,8 @@ def update_expected_user_agent(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() + await self.send_default_request() + outputs = await self.get_all_output() self.validate_outputs( outputs, modifiers=[update_expected_user_agent], @@ -853,7 +852,7 @@ def update_expected_user_agent(expected): new_sem_conv=True, ) - def test_traceresponse_header(self): + async def test_traceresponse_header(self): """Test a traceresponse header is sent when a global propagator is set.""" orig = get_global_response_propagator() @@ -861,12 +860,12 @@ def test_traceresponse_header(self): app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) - self.send_default_request() + await self.send_default_request() + response_start, response_body, *_ = await self.get_all_output() span = self.memory_exporter.get_finished_spans()[-1] self.assertEqual(trace_api.SpanKind.SERVER, span.kind) - response_start, response_body, *_ = self.get_all_output() self.assertEqual(response_body["body"], b"*") self.assertEqual(response_start["status"], 200) @@ -886,7 +885,7 @@ def test_traceresponse_header(self): set_global_response_propagator(orig) - def test_websocket(self): + async def test_websocket(self): self.scope = { "method": "GET", "type": "websocket", @@ -900,10 +899,10 @@ def test_websocket(self): } 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() + await self.send_input({"type": "websocket.connect"}) + await self.send_input({"type": "websocket.receive", "text": "ping"}) + await self.send_input({"type": "websocket.disconnect"}) + await self.get_all_output() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 6) expected = [ @@ -960,7 +959,7 @@ def test_websocket(self): self.assertEqual(span.kind, expected["kind"]) self.assertDictEqual(dict(span.attributes), expected["attributes"]) - def test_websocket_new_semconv(self): + async def test_websocket_new_semconv(self): self.scope = { "method": "GET", "type": "websocket", @@ -974,10 +973,10 @@ def test_websocket_new_semconv(self): } 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() + await self.send_input({"type": "websocket.connect"}) + await self.send_input({"type": "websocket.receive", "text": "ping"}) + await self.send_input({"type": "websocket.disconnect"}) + await self.get_all_output() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 6) expected = [ @@ -1032,7 +1031,7 @@ def test_websocket_new_semconv(self): self.assertEqual(span.kind, expected["kind"]) self.assertDictEqual(dict(span.attributes), expected["attributes"]) - def test_websocket_both_semconv(self): + async def test_websocket_both_semconv(self): self.scope = { "method": "GET", "type": "websocket", @@ -1046,10 +1045,10 @@ def test_websocket_both_semconv(self): } 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() + await self.send_input({"type": "websocket.connect"}) + await self.send_input({"type": "websocket.receive", "text": "ping"}) + await self.send_input({"type": "websocket.disconnect"}) + await self.get_all_output() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 6) expected = [ @@ -1116,7 +1115,7 @@ def test_websocket_both_semconv(self): self.assertEqual(span.kind, expected["kind"]) self.assertDictEqual(dict(span.attributes), expected["attributes"]) - def test_websocket_traceresponse_header(self): + async def test_websocket_traceresponse_header(self): """Test a traceresponse header is set for websocket messages""" orig = get_global_response_propagator() @@ -1134,10 +1133,10 @@ def test_websocket_traceresponse_header(self): } 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"}) - _, socket_send, *_ = self.get_all_output() + await self.send_input({"type": "websocket.connect"}) + await self.send_input({"type": "websocket.receive", "text": "ping"}) + await self.send_input({"type": "websocket.disconnect"}) + _, socket_send, *_ = await self.get_all_output() span = self.memory_exporter.get_finished_spans()[-1] self.assertEqual(trace_api.SpanKind.SERVER, span.kind) @@ -1156,15 +1155,15 @@ def test_websocket_traceresponse_header(self): set_global_response_propagator(orig) - def test_lifespan(self): + async def test_lifespan(self): self.scope["type"] = "lifespan" app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) - self.send_default_request() + await self.send_default_request() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) - def test_hooks(self): + async def test_hooks(self): def server_request_hook(span, scope): span.update_name("name from server hook") @@ -1191,20 +1190,23 @@ def update_expected_hook_results(expected): client_response_hook=client_response_hook, ) self.seed_app(app) - self.send_default_request() - outputs = self.get_all_output() + await self.send_default_request() + outputs = await self.get_all_output() self.validate_outputs( outputs, modifiers=[update_expected_hook_results] ) - def test_asgi_metrics(self): + async def test_asgi_metrics(self): app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) - self.send_default_request() + await self.send_default_request() + await self.get_all_output() self.seed_app(app) - self.send_default_request() + await self.send_default_request() + await self.get_all_output() self.seed_app(app) - self.send_default_request() + await self.send_default_request() + await self.get_all_output() metrics_list = self.memory_metrics_reader.get_metrics_data() number_data_point_seen = False histogram_data_point_seen = False @@ -1233,14 +1235,17 @@ def test_asgi_metrics(self): ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) - def test_asgi_metrics_new_semconv(self): + async def test_asgi_metrics_new_semconv(self): app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) - self.send_default_request() + await self.send_default_request() + await self.get_all_output() self.seed_app(app) - self.send_default_request() + await self.send_default_request() + await self.get_all_output() self.seed_app(app) - self.send_default_request() + await self.send_default_request() + await self.get_all_output() metrics_list = self.memory_metrics_reader.get_metrics_data() number_data_point_seen = False histogram_data_point_seen = False @@ -1269,14 +1274,17 @@ def test_asgi_metrics_new_semconv(self): ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) - def test_asgi_metrics_both_semconv(self): + async def test_asgi_metrics_both_semconv(self): app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) - self.send_default_request() + await self.send_default_request() + await self.get_all_output() self.seed_app(app) - self.send_default_request() + await self.send_default_request() + await self.get_all_output() self.seed_app(app) - self.send_default_request() + await self.send_default_request() + await self.get_all_output() metrics_list = self.memory_metrics_reader.get_metrics_data() number_data_point_seen = False histogram_data_point_seen = False @@ -1305,12 +1313,13 @@ def test_asgi_metrics_both_semconv(self): ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) - def test_basic_metric_success(self): + async def test_basic_metric_success(self): app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) start = default_timer() - self.send_default_request() + await self.send_default_request() duration = max(round((default_timer() - start) * 1000), 0) + await self.get_all_output() expected_duration_attributes = { "http.method": "GET", "http.host": "127.0.0.1", @@ -1352,7 +1361,7 @@ def test_basic_metric_success(self): ) self.assertEqual(point.value, 0) - def test_basic_metric_success_nonrecording_span(self): + async def test_basic_metric_success_nonrecording_span(self): mock_tracer = mock.Mock() mock_span = mock.Mock() mock_span.is_recording.return_value = False @@ -1366,8 +1375,9 @@ def test_basic_metric_success_nonrecording_span(self): app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) start = default_timer() - self.send_default_request() + await self.send_default_request() duration = max(round((default_timer() - start) * 1000), 0) + await self.get_all_output() expected_duration_attributes = { "http.method": "GET", "http.host": "127.0.0.1", @@ -1396,7 +1406,7 @@ def test_basic_metric_success_nonrecording_span(self): self.assertEqual(point.count, 1) if metric.name == "http.server.duration": self.assertAlmostEqual( - duration, point.sum, delta=5 + duration, point.sum, delta=15 ) elif ( metric.name == "http.server.response.size" @@ -1411,12 +1421,13 @@ def test_basic_metric_success_nonrecording_span(self): ) self.assertEqual(point.value, 0) - def test_basic_metric_success_new_semconv(self): + async 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() + await self.send_default_request() duration_s = max(default_timer() - start, 0) + await self.get_all_output() expected_duration_attributes = { "http.request.method": "GET", "url.scheme": "http", @@ -1458,13 +1469,14 @@ def test_basic_metric_success_new_semconv(self): ) self.assertEqual(point.value, 0) - def test_basic_metric_success_both_semconv(self): + async 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() + await self.send_default_request() duration = max(round((default_timer() - start) * 1000), 0) duration_s = max(default_timer() - start, 0) + await self.get_all_output() expected_duration_attributes_old = { "http.method": "GET", "http.host": "127.0.0.1", @@ -1546,7 +1558,7 @@ def test_basic_metric_success_both_semconv(self): ) self.assertEqual(point.value, 0) - def test_metric_target_attribute(self): + async def test_metric_target_attribute(self): expected_target = "/api/user/{id}" class TestRoute: @@ -1562,7 +1574,8 @@ async def target_asgi(scope, receive, send): app = otel_asgi.OpenTelemetryMiddleware(target_asgi) self.seed_app(app) - self.send_default_request() + await self.send_default_request() + await self.get_all_output() metrics_list = self.memory_metrics_reader.get_metrics_data() assertions = 0 for resource_metric in metrics_list.resource_metrics: @@ -1579,7 +1592,7 @@ async def target_asgi(scope, receive, send): assertions += 1 self.assertEqual(assertions, 3) - def test_no_metric_for_websockets(self): + async def test_no_metric_for_websockets(self): self.scope = { "type": "websocket", "http_version": "1.1", @@ -1592,10 +1605,10 @@ def test_no_metric_for_websockets(self): } 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() + await self.send_input({"type": "websocket.connect"}) + await self.send_input({"type": "websocket.receive", "text": "ping"}) + await self.send_input({"type": "websocket.disconnect"}) + await self.get_all_output() self.assertIsNone(self.memory_metrics_reader.get_metrics_data()) @@ -1812,8 +1825,10 @@ def test_collect_target_attribute_fastapi_starlette_invalid(self): ) -class TestWrappedApplication(AsgiTestBase): - def test_mark_span_internal_in_presence_of_span_from_other_framework(self): +class TestWrappedApplication(AsyncAsgiTestBase): + async def test_mark_span_internal_in_presence_of_span_from_other_framework( + self, + ): tracer_provider, exporter = TestBase.create_tracer_provider() tracer = tracer_provider.get_tracer(__name__) app = otel_asgi.OpenTelemetryMiddleware( @@ -1828,7 +1843,8 @@ async def wrapped_app(scope, receive, send): await app(scope, receive, send) self.seed_app(wrapped_app) - self.send_default_request() + await self.send_default_request() + await self.get_all_output() span_list = exporter.get_finished_spans() self.assertEqual(SpanKind.INTERNAL, span_list[0].kind) @@ -1845,11 +1861,11 @@ async def wrapped_app(scope, receive, send): ) -class TestAsgiApplicationRaisingError(AsgiTestBase): +class TestAsgiApplicationRaisingError(AsyncAsgiTestBase): def tearDown(self): pass - def test_asgi_issue_1883(self): + async def test_asgi_value_error_exception(self): """ Test that exception UnboundLocalError local variable 'start' referenced before assignment is not raised See https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1883 @@ -1860,11 +1876,9 @@ async def bad_app(_scope, _receive, _send): app = otel_asgi.OpenTelemetryMiddleware(bad_app) self.seed_app(app) - self.send_default_request() + await self.send_default_request() try: - asyncio.get_event_loop().run_until_complete( - self.communicator.stop() - ) + await self.communicator.wait() except ValueError as exc_info: self.assertEqual(exc_info.args[0], "whatever") except Exception as exc_info: # pylint: disable=W0703 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 43db6c3a82..15ee59a183 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py @@ -192,6 +192,7 @@ async def async_response_hook(span, request, response): """ import logging import typing +from asyncio import iscoroutinefunction from types import TracebackType import httpx @@ -731,15 +732,19 @@ def _instrument(self, **kwargs): self._original_async_client = httpx.AsyncClient request_hook = kwargs.get("request_hook") response_hook = kwargs.get("response_hook") - async_request_hook = kwargs.get("async_request_hook", request_hook) - async_response_hook = kwargs.get("async_response_hook", response_hook) + async_request_hook = kwargs.get("async_request_hook") + async_response_hook = kwargs.get("async_response_hook") if callable(request_hook): _InstrumentedClient._request_hook = request_hook - if callable(async_request_hook): + if callable(async_request_hook) and iscoroutinefunction( + async_request_hook + ): _InstrumentedAsyncClient._request_hook = async_request_hook if callable(response_hook): _InstrumentedClient._response_hook = response_hook - if callable(async_response_hook): + if callable(async_response_hook) and iscoroutinefunction( + async_response_hook + ): _InstrumentedAsyncClient._response_hook = async_response_hook tracer_provider = kwargs.get("tracer_provider") _InstrumentedClient._tracer_provider = tracer_provider diff --git a/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py b/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py index 011b5e57d2..78938eb337 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py +++ b/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py @@ -780,9 +780,15 @@ def test_custom_tracer_provider(self): HTTPXClientInstrumentor().uninstrument() def test_response_hook(self): + response_hook_key = ( + "async_response_hook" + if asyncio.iscoroutinefunction(self.response_hook) + else "response_hook" + ) + response_hook_kwargs = {response_hook_key: self.response_hook} HTTPXClientInstrumentor().instrument( tracer_provider=self.tracer_provider, - response_hook=self.response_hook, + **response_hook_kwargs, ) client = self.create_client() result = self.perform_request(self.URL, client=client) @@ -823,9 +829,15 @@ def test_response_hook_sync_async_kwargs(self): HTTPXClientInstrumentor().uninstrument() def test_request_hook(self): + request_hook_key = ( + "async_request_hook" + if asyncio.iscoroutinefunction(self.request_hook) + else "request_hook" + ) + request_hook_kwargs = {request_hook_key: self.request_hook} HTTPXClientInstrumentor().instrument( tracer_provider=self.tracer_provider, - request_hook=self.request_hook, + **request_hook_kwargs, ) client = self.create_client() result = self.perform_request(self.URL, client=client) @@ -1214,3 +1226,36 @@ def test_basic_multiple(self): self.perform_request(self.URL, client=self.client) self.perform_request(self.URL, client=self.client2) self.assert_span(num_spans=2) + + def test_async_response_hook_does_nothing_if_not_coroutine(self): + HTTPXClientInstrumentor().instrument( + tracer_provider=self.tracer_provider, + async_response_hook=_response_hook, + ) + client = self.create_client() + result = self.perform_request(self.URL, client=client) + + self.assertEqual(result.text, "Hello!") + span = self.assert_span() + self.assertEqual( + dict(span.attributes), + { + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_URL: self.URL, + SpanAttributes.HTTP_STATUS_CODE: 200, + }, + ) + HTTPXClientInstrumentor().uninstrument() + + def test_async_request_hook_does_nothing_if_not_coroutine(self): + HTTPXClientInstrumentor().instrument( + tracer_provider=self.tracer_provider, + async_request_hook=_request_hook, + ) + client = self.create_client() + result = self.perform_request(self.URL, client=client) + + self.assertEqual(result.text, "Hello!") + span = self.assert_span() + self.assertEqual(span.name, "GET") + HTTPXClientInstrumentor().uninstrument()