diff --git a/CHANGELOG.md b/CHANGELOG.md index d5237e5944..2b8329a6e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-fastapi`: Drop support for FastAPI versions earlier than `0.92` ([#3012](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3012)) +### Added +- `opentelemetry-instrumentation-aiohttp-client` Add support for HTTP metrics + ([#3517](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3517)) + ### Deprecated - Drop support for Python 3.8, bump baseline to Python 3.9. diff --git a/instrumentation/README.md b/instrumentation/README.md index fe62488fae..ef8977df86 100644 --- a/instrumentation/README.md +++ b/instrumentation/README.md @@ -2,7 +2,7 @@ | Instrumentation | Supported Packages | Metrics support | Semconv status | | --------------- | ------------------ | --------------- | -------------- | | [opentelemetry-instrumentation-aio-pika](./opentelemetry-instrumentation-aio-pika) | aio_pika >= 7.2.0, < 10.0.0 | No | development -| [opentelemetry-instrumentation-aiohttp-client](./opentelemetry-instrumentation-aiohttp-client) | aiohttp ~= 3.0 | No | migration +| [opentelemetry-instrumentation-aiohttp-client](./opentelemetry-instrumentation-aiohttp-client) | aiohttp ~= 3.0 | Yes | migration | [opentelemetry-instrumentation-aiohttp-server](./opentelemetry-instrumentation-aiohttp-server) | aiohttp ~= 3.0 | Yes | development | [opentelemetry-instrumentation-aiokafka](./opentelemetry-instrumentation-aiokafka) | aiokafka >= 0.8, < 1.0 | No | development | [opentelemetry-instrumentation-aiopg](./opentelemetry-instrumentation-aiopg) | aiopg >= 0.13.0, < 2.0.0 | No | development diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py index 08122d5b59..7bcf9fa1b4 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py @@ -90,7 +90,9 @@ def response_hook(span: Span, params: typing.Union[ import types import typing +from timeit import default_timer from typing import Collection +from urllib.parse import urlparse import aiohttp import wrapt @@ -99,11 +101,20 @@ def response_hook(span: Span, params: typing.Union[ from opentelemetry import context as context_api from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( + HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, + HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, + _client_duration_attrs_new, + _client_duration_attrs_old, + _filter_semconv_duration_attrs, _get_schema_url, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, + _report_old, + _set_http_host_client, _set_http_method, + _set_http_net_peer_name_client, + _set_http_peer_port_client, _set_http_url, _set_status, _StabilityMode, @@ -115,8 +126,13 @@ def response_hook(span: Span, params: typing.Union[ is_instrumentation_enabled, unwrap, ) +from opentelemetry.metrics import MeterProvider, get_meter from opentelemetry.propagate import inject from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE +from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.semconv.metrics.http_metrics import ( + HTTP_CLIENT_REQUEST_DURATION, +) from opentelemetry.trace import Span, SpanKind, TracerProvider, get_tracer from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.http import remove_url_credentials, sanitize_method @@ -172,11 +188,14 @@ def _set_http_status_code_attribute( ) +# pylint: disable=too-many-locals +# pylint: disable=too-many-statements def create_trace_config( url_filter: _UrlFilterT = None, request_hook: _RequestHookT = None, response_hook: _ResponseHookT = None, tracer_provider: TracerProvider = None, + meter_provider: MeterProvider = None, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ) -> aiohttp.TraceConfig: """Create an aiohttp-compatible trace configuration. @@ -205,6 +224,7 @@ def create_trace_config( :param Callable request_hook: Optional callback that can modify span name and request params. :param Callable response_hook: Optional callback that can modify span name and response params. :param tracer_provider: optional TracerProvider from which to get a Tracer + :param meter_provider: optional Meter provider to use :return: An object suitable for use with :py:class:`aiohttp.ClientSession`. :rtype: :py:class:`aiohttp.TraceConfig` @@ -214,20 +234,70 @@ def create_trace_config( # Explicitly specify the type for the `request_hook` and `response_hook` param and rtype to work # around this issue. + schema_url = _get_schema_url(sem_conv_opt_in_mode) + tracer = get_tracer( __name__, __version__, tracer_provider, - schema_url=_get_schema_url(sem_conv_opt_in_mode), + schema_url=schema_url, + ) + + meter = get_meter( + __name__, + __version__, + meter_provider, + schema_url, ) - # TODO: Use this when we have durations for aiohttp-client + start_time = 0 + + duration_histogram_old = None + if _report_old(sem_conv_opt_in_mode): + duration_histogram_old = meter.create_histogram( + name=MetricInstruments.HTTP_CLIENT_DURATION, + unit="ms", + description="measures the duration of the outbound HTTP request", + explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, + ) + duration_histogram_new = None + if _report_new(sem_conv_opt_in_mode): + duration_histogram_new = meter.create_histogram( + name=HTTP_CLIENT_REQUEST_DURATION, + unit="s", + description="Duration of HTTP client requests.", + explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, + ) + metric_attributes = {} def _end_trace(trace_config_ctx: types.SimpleNamespace): + elapsed_time = max(default_timer() - trace_config_ctx.start_time, 0) context_api.detach(trace_config_ctx.token) trace_config_ctx.span.end() + if trace_config_ctx.duration_histogram_old is not None: + duration_attrs_old = _filter_semconv_duration_attrs( + metric_attributes, + _client_duration_attrs_old, + _client_duration_attrs_new, + _StabilityMode.DEFAULT, + ) + trace_config_ctx.duration_histogram_old.record( + max(round(elapsed_time * 1000), 0), + attributes=duration_attrs_old, + ) + if trace_config_ctx.duration_histogram_new is not None: + duration_attrs_new = _filter_semconv_duration_attrs( + metric_attributes, + _client_duration_attrs_old, + _client_duration_attrs_new, + _StabilityMode.HTTP, + ) + trace_config_ctx.duration_histogram_new.record( + elapsed_time, attributes=duration_attrs_new + ) + async def on_request_start( unused_session: aiohttp.ClientSession, trace_config_ctx: types.SimpleNamespace, @@ -237,6 +307,7 @@ async def on_request_start( trace_config_ctx.span = None return + trace_config_ctx.start_time = default_timer() method = params.method request_span_name = _get_span_name(method) request_url = ( @@ -252,8 +323,44 @@ async def on_request_start( sanitize_method(method), sem_conv_opt_in_mode, ) + _set_http_method( + metric_attributes, + method, + sanitize_method(method), + sem_conv_opt_in_mode, + ) _set_http_url(span_attributes, request_url, sem_conv_opt_in_mode) + try: + parsed_url = urlparse(request_url) + if parsed_url.hostname: + _set_http_host_client( + metric_attributes, + parsed_url.hostname, + sem_conv_opt_in_mode, + ) + _set_http_net_peer_name_client( + metric_attributes, + parsed_url.hostname, + sem_conv_opt_in_mode, + ) + if _report_new(sem_conv_opt_in_mode): + _set_http_host_client( + span_attributes, + parsed_url.hostname, + sem_conv_opt_in_mode, + ) + if parsed_url.port: + _set_http_peer_port_client( + metric_attributes, parsed_url.port, sem_conv_opt_in_mode + ) + if _report_new(sem_conv_opt_in_mode): + _set_http_peer_port_client( + span_attributes, parsed_url.port, sem_conv_opt_in_mode + ) + except ValueError: + pass + trace_config_ctx.span = trace_config_ctx.tracer.start_span( request_span_name, kind=SpanKind.CLIENT, attributes=span_attributes ) @@ -298,6 +405,7 @@ async def on_request_exception( exc_type = type(params.exception).__qualname__ if _report_new(sem_conv_opt_in_mode): trace_config_ctx.span.set_attribute(ERROR_TYPE, exc_type) + metric_attributes[ERROR_TYPE] = exc_type trace_config_ctx.span.set_status( Status(StatusCode.ERROR, exc_type) @@ -312,7 +420,12 @@ async def on_request_exception( def _trace_config_ctx_factory(**kwargs): kwargs.setdefault("trace_request_ctx", {}) return types.SimpleNamespace( - tracer=tracer, url_filter=url_filter, **kwargs + tracer=tracer, + url_filter=url_filter, + start_time=start_time, + duration_histogram_old=duration_histogram_old, + duration_histogram_new=duration_histogram_new, + **kwargs, ) trace_config = aiohttp.TraceConfig( @@ -328,6 +441,7 @@ def _trace_config_ctx_factory(**kwargs): def _instrument( tracer_provider: TracerProvider = None, + meter_provider: MeterProvider = None, url_filter: _UrlFilterT = None, request_hook: _RequestHookT = None, response_hook: _ResponseHookT = None, @@ -357,6 +471,7 @@ def instrumented_init(wrapped, instance, args, kwargs): request_hook=request_hook, response_hook=response_hook, tracer_provider=tracer_provider, + meter_provider=meter_provider, sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) trace_config._is_instrumented_by_opentelemetry = True @@ -401,6 +516,7 @@ def _instrument(self, **kwargs): Args: **kwargs: Optional arguments ``tracer_provider``: a TracerProvider, defaults to global + ``meter_provider``: a MeterProvider, defaults to global ``url_filter``: A callback to process the requested URL prior to adding it as a span attribute. This can be useful to remove sensitive data such as API keys or user personal information. @@ -415,6 +531,7 @@ def _instrument(self, **kwargs): ) _instrument( tracer_provider=kwargs.get("tracer_provider"), + meter_provider=kwargs.get("meter_provider"), url_filter=kwargs.get("url_filter"), request_hook=kwargs.get("request_hook"), response_hook=kwargs.get("response_hook"), diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/package.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/package.py index 98ae4b0874..7346595713 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/package.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/package.py @@ -15,6 +15,6 @@ _instruments = ("aiohttp ~= 3.0",) -_supports_metrics = False +_supports_metrics = True _semconv_status = "migration" diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py index 98351d8339..042f4502bb 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=too-many-lines + import asyncio import contextlib import typing @@ -28,6 +30,8 @@ from opentelemetry import trace as trace_api from opentelemetry.instrumentation import aiohttp_client from opentelemetry.instrumentation._semconv import ( + HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, + HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, _StabilityMode, @@ -37,10 +41,19 @@ ) from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.semconv._incubating.attributes.http_attributes import ( + HTTP_HOST, HTTP_METHOD, HTTP_STATUS_CODE, HTTP_URL, ) +from opentelemetry.semconv._incubating.attributes.net_attributes import ( + NET_PEER_NAME, + NET_PEER_PORT, +) +from opentelemetry.semconv._incubating.attributes.server_attributes import ( + SERVER_ADDRESS, + SERVER_PORT, +) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, @@ -88,7 +101,7 @@ def setUp(self): super().setUp() _OpenTelemetrySemanticConventionStability._initialized = False - def assert_spans(self, spans, num_spans=1): + def _assert_spans(self, spans, num_spans=1): finished_spans = self.memory_exporter.get_finished_spans() self.assertEqual(num_spans, len(finished_spans)) self.assertEqual( @@ -103,6 +116,11 @@ def assert_spans(self, spans, num_spans=1): spans, ) + def _assert_metrics(self, num_metrics: int = 1): + metrics = self.get_sorted_metrics() + self.assertEqual(len(metrics), num_metrics) + return metrics + @staticmethod def _http_request( trace_config, @@ -130,6 +148,7 @@ async def client_request(server: aiohttp.test_utils.TestServer): return run_with_test_server(client_request, url, handler) def test_status_codes(self): + index = 0 for status_code, span_status in self._test_status_codes: with self.subTest(status_code=status_code): path = "test-path?query=param#foobar" @@ -144,11 +163,30 @@ def test_status_codes(self): HTTP_URL: url, HTTP_STATUS_CODE: status_code, } + spans = [("GET", (span_status, None), attributes)] - self.assert_spans(spans) + self._assert_spans(spans) self.memory_exporter.clear() + metrics = self._assert_metrics(1) + duration_data_point = metrics[0].data.data_points[index] + self.assertEqual( + dict(duration_data_point.attributes), + { + HTTP_STATUS_CODE: status_code, + HTTP_METHOD: "GET", + HTTP_HOST: host, + NET_PEER_NAME: host, + NET_PEER_PORT: port, + }, + ) + self.assertEqual( + duration_data_point.explicit_bounds, + HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, + ) + index += 1 def test_status_codes_new_semconv(self): + index = 0 for status_code, span_status in self._test_status_codes: with self.subTest(status_code=status_code): path = "test-path?query=param#foobar" @@ -164,14 +202,39 @@ def test_status_codes_new_semconv(self): HTTP_REQUEST_METHOD: "GET", URL_FULL: url, HTTP_RESPONSE_STATUS_CODE: status_code, + SERVER_ADDRESS: host, + SERVER_PORT: port, } if status_code >= 400: attributes[ERROR_TYPE] = str(status_code.value) spans = [("GET", (span_status, None), attributes)] - self.assert_spans(spans) + self._assert_spans(spans) self.memory_exporter.clear() + metrics = self._assert_metrics(1) + duration_data_point = metrics[0].data.data_points[index] + self.assertEqual( + duration_data_point.attributes.get( + HTTP_RESPONSE_STATUS_CODE + ), + status_code, + ) + self.assertEqual( + duration_data_point.attributes.get(HTTP_REQUEST_METHOD), + "GET", + ) + if status_code >= 400: + self.assertEqual( + duration_data_point.attributes.get(ERROR_TYPE), + str(status_code.value), + ) + self.assertEqual( + duration_data_point.explicit_bounds, + HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, + ) + index += 1 def test_status_codes_both_semconv(self): + index = 0 for status_code, span_status in self._test_status_codes: with self.subTest(status_code=status_code): path = "test-path?query=param#foobar" @@ -186,17 +249,78 @@ def test_status_codes_both_semconv(self): attributes = { HTTP_REQUEST_METHOD: "GET", HTTP_METHOD: "GET", + HTTP_HOST: host, URL_FULL: url, HTTP_URL: url, HTTP_RESPONSE_STATUS_CODE: status_code, HTTP_STATUS_CODE: status_code, + SERVER_ADDRESS: host, + SERVER_PORT: port, + NET_PEER_PORT: port, } + if status_code >= 400: attributes[ERROR_TYPE] = str(status_code.value) spans = [("GET", (span_status, None), attributes)] - self.assert_spans(spans, 1) + self._assert_spans(spans, 1) self.memory_exporter.clear() + metrics = self._assert_metrics(2) + duration_data_point = metrics[0].data.data_points[index] + self.assertEqual( + duration_data_point.attributes.get(HTTP_STATUS_CODE), + status_code, + ) + self.assertEqual( + duration_data_point.attributes.get(HTTP_METHOD), + "GET", + ) + self.assertEqual( + duration_data_point.attributes.get(ERROR_TYPE), + None, + ) + duration_data_point = metrics[1].data.data_points[index] + self.assertEqual( + duration_data_point.attributes.get( + HTTP_RESPONSE_STATUS_CODE + ), + status_code, + ) + self.assertEqual( + duration_data_point.attributes.get(HTTP_REQUEST_METHOD), + "GET", + ) + if status_code >= 400: + self.assertEqual( + duration_data_point.attributes.get(ERROR_TYPE), + str(status_code.value), + ) + index += 1 + + def test_metrics(self): + with self.subTest(status_code=200): + host, port = self._http_request( + trace_config=aiohttp_client.create_trace_config(), + url="/test-path?query=param#foobar", + status_code=200, + ) + metrics = self._assert_metrics(1) + self.assertEqual(len(metrics[0].data.data_points), 1) + duration_data_point = metrics[0].data.data_points[0] + self.assertEqual( + dict(metrics[0].data.data_points[0].attributes), + { + HTTP_STATUS_CODE: 200, + HTTP_METHOD: "GET", + HTTP_HOST: host, + NET_PEER_NAME: host, + NET_PEER_PORT: port, + }, + ) + self.assertEqual(duration_data_point.count, 1) + self.assertTrue(duration_data_point.min > 0) + self.assertTrue(duration_data_point.max > 0) + self.assertTrue(duration_data_point.sum > 0) def test_schema_url(self): with self.subTest(status_code=200): @@ -319,7 +443,7 @@ def strip_query_params(url: yarl.URL) -> str: status_code=HTTPStatus.OK, ) - self.assert_spans( + self._assert_spans( [ ( "GET", @@ -353,7 +477,7 @@ async def do_request(url): with self.assertRaises(aiohttp.ClientConnectorError): loop.run_until_complete(do_request(url)) - self.assert_spans( + self._assert_spans( [ ( "GET", @@ -379,7 +503,7 @@ async def request_handler(request): span = self.memory_exporter.get_finished_spans()[0] self.assertEqual(len(span.events), 1) self.assertEqual(span.events[0].name, "exception") - self.assert_spans( + self._assert_spans( [ ( "GET", @@ -391,6 +515,17 @@ async def request_handler(request): ) ] ) + metrics = self._assert_metrics(1) + duration_data_point = metrics[0].data.data_points[0] + self.assertEqual( + dict(duration_data_point.attributes), + { + HTTP_METHOD: "GET", + HTTP_HOST: host, + NET_PEER_NAME: host, + NET_PEER_PORT: port, + }, + ) def test_basic_exception_new_semconv(self): async def request_handler(request): @@ -406,7 +541,7 @@ async def request_handler(request): span = self.memory_exporter.get_finished_spans()[0] self.assertEqual(len(span.events), 1) self.assertEqual(span.events[0].name, "exception") - self.assert_spans( + self._assert_spans( [ ( "GET", @@ -415,10 +550,23 @@ async def request_handler(request): HTTP_REQUEST_METHOD: "GET", URL_FULL: f"http://{host}:{port}/test", ERROR_TYPE: "ServerDisconnectedError", + SERVER_ADDRESS: host, + SERVER_PORT: port, }, ) ] ) + metrics = self._assert_metrics(1) + duration_data_point = metrics[0].data.data_points[0] + self.assertEqual( + dict(duration_data_point.attributes), + { + HTTP_REQUEST_METHOD: "GET", + ERROR_TYPE: "ServerDisconnectedError", + SERVER_ADDRESS: host, + SERVER_PORT: port, + }, + ) def test_basic_exception_both_semconv(self): async def request_handler(request): @@ -434,7 +582,7 @@ async def request_handler(request): span = self.memory_exporter.get_finished_spans()[0] self.assertEqual(len(span.events), 1) self.assertEqual(span.events[0].name, "exception") - self.assert_spans( + self._assert_spans( [ ( "GET", @@ -445,10 +593,35 @@ async def request_handler(request): ERROR_TYPE: "ServerDisconnectedError", HTTP_METHOD: "GET", HTTP_URL: f"http://{host}:{port}/test", + HTTP_HOST: host, + SERVER_ADDRESS: host, + SERVER_PORT: port, + NET_PEER_PORT: port, }, ) ] ) + metrics = self._assert_metrics(2) + duration_data_point = metrics[0].data.data_points[0] + self.assertEqual( + dict(duration_data_point.attributes), + { + HTTP_METHOD: "GET", + HTTP_HOST: host, + NET_PEER_NAME: host, + NET_PEER_PORT: port, + }, + ) + duration_data_point = metrics[1].data.data_points[0] + self.assertEqual( + dict(duration_data_point.attributes), + { + HTTP_REQUEST_METHOD: "GET", + ERROR_TYPE: "ServerDisconnectedError", + SERVER_ADDRESS: host, + SERVER_PORT: port, + }, + ) def test_timeout(self): async def request_handler(request): @@ -463,7 +636,7 @@ async def request_handler(request): timeout=aiohttp.ClientTimeout(sock_read=0.01), ) - self.assert_spans( + self._assert_spans( [ ( "GET", @@ -490,7 +663,7 @@ async def request_handler(request): max_redirects=2, ) - self.assert_spans( + self._assert_spans( [ ( "GET", @@ -526,7 +699,7 @@ async def do_request(url): loop = asyncio.get_event_loop() loop.run_until_complete(do_request(url)) - self.assert_spans( + self._assert_spans( [ ( "HTTP", @@ -568,7 +741,7 @@ async def do_request(url): loop = asyncio.get_event_loop() loop.run_until_complete(do_request(url)) - self.assert_spans( + self._assert_spans( [ ( "HTTP", @@ -581,6 +754,8 @@ async def do_request(url): ), HTTP_REQUEST_METHOD_ORIGINAL: "NONSTANDARD", ERROR_TYPE: "405", + SERVER_ADDRESS: "localhost", + SERVER_PORT: 5000, }, ) ] @@ -611,7 +786,7 @@ async def do_request(url): loop = asyncio.get_event_loop() loop.run_until_complete(do_request(url)) - self.assert_spans( + self._assert_spans( [ ( "GET", @@ -652,7 +827,7 @@ async def default_request(server: aiohttp.test_utils.TestServer): return default_request - def assert_spans(self, num_spans: int): + def _assert_spans(self, num_spans: int): finished_spans = self.memory_exporter.get_finished_spans() self.assertEqual(num_spans, len(finished_spans)) if num_spans == 0: @@ -661,11 +836,16 @@ def assert_spans(self, num_spans: int): return finished_spans[0] return finished_spans + def _assert_metrics(self, num_metrics: int = 1): + metrics = self.get_sorted_metrics() + self.assertEqual(len(metrics), num_metrics) + return metrics + def test_instrument(self): host, port = run_with_test_server( self.get_default_request(), self.URL, self.default_handler ) - span = self.assert_spans(1) + span = self._assert_spans(1) self.assertEqual("GET", span.name) self.assertEqual("GET", span.attributes[HTTP_METHOD]) self.assertEqual( @@ -673,6 +853,19 @@ def test_instrument(self): span.attributes[HTTP_URL], ) self.assertEqual(200, span.attributes[HTTP_STATUS_CODE]) + metrics = self._assert_metrics(1) + duration_data_point = metrics[0].data.data_points[0] + self.assertEqual(duration_data_point.count, 1) + self.assertEqual( + dict(duration_data_point.attributes), + { + HTTP_HOST: host, + HTTP_STATUS_CODE: 200, + HTTP_METHOD: "GET", + NET_PEER_NAME: host, + NET_PEER_PORT: port, + }, + ) def test_instrument_new_semconv(self): AioHttpClientInstrumentor().uninstrument() @@ -683,7 +876,7 @@ def test_instrument_new_semconv(self): host, port = run_with_test_server( self.get_default_request(), self.URL, self.default_handler ) - span = self.assert_spans(1) + span = self._assert_spans(1) self.assertEqual("GET", span.name) self.assertEqual("GET", span.attributes[HTTP_REQUEST_METHOD]) self.assertEqual( @@ -691,6 +884,18 @@ def test_instrument_new_semconv(self): span.attributes[URL_FULL], ) self.assertEqual(200, span.attributes[HTTP_RESPONSE_STATUS_CODE]) + metrics = self._assert_metrics(1) + duration_data_point = metrics[0].data.data_points[0] + self.assertEqual(duration_data_point.count, 1) + self.assertEqual( + dict(duration_data_point.attributes), + { + HTTP_RESPONSE_STATUS_CODE: 200, + HTTP_REQUEST_METHOD: "GET", + SERVER_ADDRESS: host, + SERVER_PORT: port, + }, + ) def test_instrument_both_semconv(self): AioHttpClientInstrumentor().uninstrument() @@ -702,17 +907,47 @@ def test_instrument_both_semconv(self): self.get_default_request(), self.URL, self.default_handler ) url = f"http://{host}:{port}/test-path" - attributes = { - HTTP_REQUEST_METHOD: "GET", - HTTP_METHOD: "GET", - URL_FULL: url, - HTTP_URL: url, - HTTP_RESPONSE_STATUS_CODE: 200, - HTTP_STATUS_CODE: 200, - } - span = self.assert_spans(1) + span = self._assert_spans(1) self.assertEqual("GET", span.name) - self.assertEqual(span.attributes, attributes) + self.assertEqual( + dict(span.attributes), + { + HTTP_REQUEST_METHOD: "GET", + HTTP_METHOD: "GET", + HTTP_HOST: host, + URL_FULL: url, + HTTP_URL: url, + HTTP_RESPONSE_STATUS_CODE: 200, + HTTP_STATUS_CODE: 200, + SERVER_ADDRESS: host, + SERVER_PORT: port, + NET_PEER_PORT: port, + }, + ) + metrics = self._assert_metrics(2) + duration_data_point = metrics[0].data.data_points[0] + self.assertEqual(duration_data_point.count, 1) + self.assertEqual( + dict(duration_data_point.attributes), + { + HTTP_STATUS_CODE: 200, + HTTP_METHOD: "GET", + HTTP_HOST: host, + NET_PEER_NAME: host, + NET_PEER_PORT: port, + }, + ) + duration_data_point = metrics[1].data.data_points[0] + self.assertEqual(duration_data_point.count, 1) + self.assertEqual( + dict(duration_data_point.attributes), + { + HTTP_RESPONSE_STATUS_CODE: 200, + HTTP_REQUEST_METHOD: "GET", + SERVER_ADDRESS: host, + SERVER_PORT: port, + }, + ) def test_instrument_with_custom_trace_config(self): trace_config = aiohttp.TraceConfig() @@ -729,7 +964,7 @@ async def make_request(server: aiohttp.test_utils.TestServer): await session.get(TestAioHttpClientInstrumentor.URL) run_with_test_server(make_request, self.URL, self.default_handler) - self.assert_spans(1) + self._assert_spans(1) def test_every_request_by_new_session_creates_one_span(self): async def make_request(server: aiohttp.test_utils.TestServer): @@ -743,7 +978,7 @@ async def make_request(server: aiohttp.test_utils.TestServer): run_with_test_server( make_request, self.URL, self.default_handler ) - self.assert_spans(1) + self._assert_spans(1) def test_instrument_with_existing_trace_config(self): trace_config = aiohttp.TraceConfig() @@ -760,7 +995,7 @@ async def create_session(server: aiohttp.test_utils.TestServer): await session.get(TestAioHttpClientInstrumentor.URL) run_with_test_server(create_session, self.URL, self.default_handler) - self.assert_spans(1) + self._assert_spans(1) def test_no_op_tracer_provider(self): AioHttpClientInstrumentor().uninstrument() @@ -780,13 +1015,13 @@ def test_uninstrument(self): self.get_default_request(), self.URL, self.default_handler ) - self.assert_spans(0) + self._assert_spans(0) AioHttpClientInstrumentor().instrument() run_with_test_server( self.get_default_request(), self.URL, self.default_handler ) - self.assert_spans(1) + self._assert_spans(1) def test_uninstrument_session(self): async def uninstrument_request(server: aiohttp.test_utils.TestServer): @@ -798,19 +1033,19 @@ async def uninstrument_request(server: aiohttp.test_utils.TestServer): run_with_test_server( uninstrument_request, self.URL, self.default_handler ) - self.assert_spans(0) + self._assert_spans(0) run_with_test_server( self.get_default_request(), self.URL, self.default_handler ) - self.assert_spans(1) + self._assert_spans(1) def test_suppress_instrumentation(self): with suppress_instrumentation(): run_with_test_server( self.get_default_request(), self.URL, self.default_handler ) - self.assert_spans(0) + self._assert_spans(0) @staticmethod async def suppressed_request(server: aiohttp.test_utils.TestServer): @@ -822,7 +1057,7 @@ def test_suppress_instrumentation_after_creation(self): run_with_test_server( self.suppressed_request, self.URL, self.default_handler ) - self.assert_spans(0) + self._assert_spans(0) def test_suppress_instrumentation_with_server_exception(self): # pylint:disable=unused-argument @@ -832,7 +1067,7 @@ async def raising_handler(request): run_with_test_server( self.suppressed_request, self.URL, raising_handler ) - self.assert_spans(0) + self._assert_spans(0) def test_url_filter(self): def strip_query_params(url: yarl.URL) -> str: @@ -845,7 +1080,7 @@ def strip_query_params(url: yarl.URL) -> str: host, port = run_with_test_server( self.get_default_request(url), url, self.default_handler ) - span = self.assert_spans(1) + span = self._assert_spans(1) self.assertEqual( f"http://{host}:{port}/test-path", span.attributes[HTTP_URL], @@ -873,7 +1108,7 @@ def response_hook( run_with_test_server( self.get_default_request(url), url, self.default_handler ) - span = self.assert_spans(1) + span = self._assert_spans(1) self.assertEqual("GET - /test-path", span.name) self.assertIn("response_hook_attr", span.attributes) self.assertEqual(span.attributes["response_hook_attr"], "value")