opentelemetry-instrumentation-aiohttp-client 0.54b0__tar.gz → 0.55b0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opentelemetry-instrumentation-aiohttp-client
3
- Version: 0.54b0
3
+ Version: 0.55b0
4
4
  Summary: OpenTelemetry aiohttp client instrumentation
5
5
  Project-URL: Homepage, https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-aiohttp-client
6
6
  Project-URL: Repository, https://github.com/open-telemetry/opentelemetry-python-contrib
@@ -12,17 +12,16 @@ Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: Apache Software License
13
13
  Classifier: Programming Language :: Python
14
14
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.8
16
15
  Classifier: Programming Language :: Python :: 3.9
17
16
  Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
19
  Classifier: Programming Language :: Python :: 3.13
21
- Requires-Python: >=3.8
20
+ Requires-Python: >=3.9
22
21
  Requires-Dist: opentelemetry-api~=1.12
23
- Requires-Dist: opentelemetry-instrumentation==0.54b0
24
- Requires-Dist: opentelemetry-semantic-conventions==0.54b0
25
- Requires-Dist: opentelemetry-util-http==0.54b0
22
+ Requires-Dist: opentelemetry-instrumentation==0.55b0
23
+ Requires-Dist: opentelemetry-semantic-conventions==0.55b0
24
+ Requires-Dist: opentelemetry-util-http==0.55b0
26
25
  Requires-Dist: wrapt<2.0.0,>=1.0.0
27
26
  Provides-Extra: instruments
28
27
  Requires-Dist: aiohttp~=3.0; extra == 'instruments'
@@ -8,7 +8,7 @@ dynamic = ["version"]
8
8
  description = "OpenTelemetry aiohttp client instrumentation"
9
9
  readme = "README.rst"
10
10
  license = "Apache-2.0"
11
- requires-python = ">=3.8"
11
+ requires-python = ">=3.9"
12
12
  authors = [
13
13
  { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" },
14
14
  ]
@@ -18,7 +18,6 @@ classifiers = [
18
18
  "License :: OSI Approved :: Apache Software License",
19
19
  "Programming Language :: Python",
20
20
  "Programming Language :: Python :: 3",
21
- "Programming Language :: Python :: 3.8",
22
21
  "Programming Language :: Python :: 3.9",
23
22
  "Programming Language :: Python :: 3.10",
24
23
  "Programming Language :: Python :: 3.11",
@@ -27,9 +26,9 @@ classifiers = [
27
26
  ]
28
27
  dependencies = [
29
28
  "opentelemetry-api ~= 1.12",
30
- "opentelemetry-instrumentation == 0.54b0",
31
- "opentelemetry-semantic-conventions == 0.54b0",
32
- "opentelemetry-util-http == 0.54b0",
29
+ "opentelemetry-instrumentation == 0.55b0",
30
+ "opentelemetry-semantic-conventions == 0.55b0",
31
+ "opentelemetry-util-http == 0.55b0",
33
32
  "wrapt >= 1.0.0, < 2.0.0",
34
33
  ]
35
34
 
@@ -90,7 +90,9 @@ API
90
90
 
91
91
  import types
92
92
  import typing
93
+ from timeit import default_timer
93
94
  from typing import Collection
95
+ from urllib.parse import urlparse
94
96
 
95
97
  import aiohttp
96
98
  import wrapt
@@ -99,11 +101,20 @@ import yarl
99
101
  from opentelemetry import context as context_api
100
102
  from opentelemetry import trace
101
103
  from opentelemetry.instrumentation._semconv import (
104
+ HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
105
+ HTTP_DURATION_HISTOGRAM_BUCKETS_OLD,
106
+ _client_duration_attrs_new,
107
+ _client_duration_attrs_old,
108
+ _filter_semconv_duration_attrs,
102
109
  _get_schema_url,
103
110
  _OpenTelemetrySemanticConventionStability,
104
111
  _OpenTelemetryStabilitySignalType,
105
112
  _report_new,
113
+ _report_old,
114
+ _set_http_host_client,
106
115
  _set_http_method,
116
+ _set_http_net_peer_name_client,
117
+ _set_http_peer_port_client,
107
118
  _set_http_url,
108
119
  _set_status,
109
120
  _StabilityMode,
@@ -115,8 +126,13 @@ from opentelemetry.instrumentation.utils import (
115
126
  is_instrumentation_enabled,
116
127
  unwrap,
117
128
  )
129
+ from opentelemetry.metrics import MeterProvider, get_meter
118
130
  from opentelemetry.propagate import inject
119
131
  from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
132
+ from opentelemetry.semconv.metrics import MetricInstruments
133
+ from opentelemetry.semconv.metrics.http_metrics import (
134
+ HTTP_CLIENT_REQUEST_DURATION,
135
+ )
120
136
  from opentelemetry.trace import Span, SpanKind, TracerProvider, get_tracer
121
137
  from opentelemetry.trace.status import Status, StatusCode
122
138
  from opentelemetry.util.http import remove_url_credentials, sanitize_method
@@ -172,11 +188,14 @@ def _set_http_status_code_attribute(
172
188
  )
173
189
 
174
190
 
191
+ # pylint: disable=too-many-locals
192
+ # pylint: disable=too-many-statements
175
193
  def create_trace_config(
176
194
  url_filter: _UrlFilterT = None,
177
195
  request_hook: _RequestHookT = None,
178
196
  response_hook: _ResponseHookT = None,
179
197
  tracer_provider: TracerProvider = None,
198
+ meter_provider: MeterProvider = None,
180
199
  sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
181
200
  ) -> aiohttp.TraceConfig:
182
201
  """Create an aiohttp-compatible trace configuration.
@@ -205,6 +224,7 @@ def create_trace_config(
205
224
  :param Callable request_hook: Optional callback that can modify span name and request params.
206
225
  :param Callable response_hook: Optional callback that can modify span name and response params.
207
226
  :param tracer_provider: optional TracerProvider from which to get a Tracer
227
+ :param meter_provider: optional Meter provider to use
208
228
 
209
229
  :return: An object suitable for use with :py:class:`aiohttp.ClientSession`.
210
230
  :rtype: :py:class:`aiohttp.TraceConfig`
@@ -214,20 +234,70 @@ def create_trace_config(
214
234
  # Explicitly specify the type for the `request_hook` and `response_hook` param and rtype to work
215
235
  # around this issue.
216
236
 
237
+ schema_url = _get_schema_url(sem_conv_opt_in_mode)
238
+
217
239
  tracer = get_tracer(
218
240
  __name__,
219
241
  __version__,
220
242
  tracer_provider,
221
- schema_url=_get_schema_url(sem_conv_opt_in_mode),
243
+ schema_url=schema_url,
244
+ )
245
+
246
+ meter = get_meter(
247
+ __name__,
248
+ __version__,
249
+ meter_provider,
250
+ schema_url,
222
251
  )
223
252
 
224
- # TODO: Use this when we have durations for aiohttp-client
253
+ start_time = 0
254
+
255
+ duration_histogram_old = None
256
+ if _report_old(sem_conv_opt_in_mode):
257
+ duration_histogram_old = meter.create_histogram(
258
+ name=MetricInstruments.HTTP_CLIENT_DURATION,
259
+ unit="ms",
260
+ description="measures the duration of the outbound HTTP request",
261
+ explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_OLD,
262
+ )
263
+ duration_histogram_new = None
264
+ if _report_new(sem_conv_opt_in_mode):
265
+ duration_histogram_new = meter.create_histogram(
266
+ name=HTTP_CLIENT_REQUEST_DURATION,
267
+ unit="s",
268
+ description="Duration of HTTP client requests.",
269
+ explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
270
+ )
271
+
225
272
  metric_attributes = {}
226
273
 
227
274
  def _end_trace(trace_config_ctx: types.SimpleNamespace):
275
+ elapsed_time = max(default_timer() - trace_config_ctx.start_time, 0)
228
276
  context_api.detach(trace_config_ctx.token)
229
277
  trace_config_ctx.span.end()
230
278
 
279
+ if trace_config_ctx.duration_histogram_old is not None:
280
+ duration_attrs_old = _filter_semconv_duration_attrs(
281
+ metric_attributes,
282
+ _client_duration_attrs_old,
283
+ _client_duration_attrs_new,
284
+ _StabilityMode.DEFAULT,
285
+ )
286
+ trace_config_ctx.duration_histogram_old.record(
287
+ max(round(elapsed_time * 1000), 0),
288
+ attributes=duration_attrs_old,
289
+ )
290
+ if trace_config_ctx.duration_histogram_new is not None:
291
+ duration_attrs_new = _filter_semconv_duration_attrs(
292
+ metric_attributes,
293
+ _client_duration_attrs_old,
294
+ _client_duration_attrs_new,
295
+ _StabilityMode.HTTP,
296
+ )
297
+ trace_config_ctx.duration_histogram_new.record(
298
+ elapsed_time, attributes=duration_attrs_new
299
+ )
300
+
231
301
  async def on_request_start(
232
302
  unused_session: aiohttp.ClientSession,
233
303
  trace_config_ctx: types.SimpleNamespace,
@@ -237,6 +307,7 @@ def create_trace_config(
237
307
  trace_config_ctx.span = None
238
308
  return
239
309
 
310
+ trace_config_ctx.start_time = default_timer()
240
311
  method = params.method
241
312
  request_span_name = _get_span_name(method)
242
313
  request_url = (
@@ -252,8 +323,44 @@ def create_trace_config(
252
323
  sanitize_method(method),
253
324
  sem_conv_opt_in_mode,
254
325
  )
326
+ _set_http_method(
327
+ metric_attributes,
328
+ method,
329
+ sanitize_method(method),
330
+ sem_conv_opt_in_mode,
331
+ )
255
332
  _set_http_url(span_attributes, request_url, sem_conv_opt_in_mode)
256
333
 
334
+ try:
335
+ parsed_url = urlparse(request_url)
336
+ if parsed_url.hostname:
337
+ _set_http_host_client(
338
+ metric_attributes,
339
+ parsed_url.hostname,
340
+ sem_conv_opt_in_mode,
341
+ )
342
+ _set_http_net_peer_name_client(
343
+ metric_attributes,
344
+ parsed_url.hostname,
345
+ sem_conv_opt_in_mode,
346
+ )
347
+ if _report_new(sem_conv_opt_in_mode):
348
+ _set_http_host_client(
349
+ span_attributes,
350
+ parsed_url.hostname,
351
+ sem_conv_opt_in_mode,
352
+ )
353
+ if parsed_url.port:
354
+ _set_http_peer_port_client(
355
+ metric_attributes, parsed_url.port, sem_conv_opt_in_mode
356
+ )
357
+ if _report_new(sem_conv_opt_in_mode):
358
+ _set_http_peer_port_client(
359
+ span_attributes, parsed_url.port, sem_conv_opt_in_mode
360
+ )
361
+ except ValueError:
362
+ pass
363
+
257
364
  trace_config_ctx.span = trace_config_ctx.tracer.start_span(
258
365
  request_span_name, kind=SpanKind.CLIENT, attributes=span_attributes
259
366
  )
@@ -298,6 +405,7 @@ def create_trace_config(
298
405
  exc_type = type(params.exception).__qualname__
299
406
  if _report_new(sem_conv_opt_in_mode):
300
407
  trace_config_ctx.span.set_attribute(ERROR_TYPE, exc_type)
408
+ metric_attributes[ERROR_TYPE] = exc_type
301
409
 
302
410
  trace_config_ctx.span.set_status(
303
411
  Status(StatusCode.ERROR, exc_type)
@@ -312,7 +420,12 @@ def create_trace_config(
312
420
  def _trace_config_ctx_factory(**kwargs):
313
421
  kwargs.setdefault("trace_request_ctx", {})
314
422
  return types.SimpleNamespace(
315
- tracer=tracer, url_filter=url_filter, **kwargs
423
+ tracer=tracer,
424
+ url_filter=url_filter,
425
+ start_time=start_time,
426
+ duration_histogram_old=duration_histogram_old,
427
+ duration_histogram_new=duration_histogram_new,
428
+ **kwargs,
316
429
  )
317
430
 
318
431
  trace_config = aiohttp.TraceConfig(
@@ -328,6 +441,7 @@ def create_trace_config(
328
441
 
329
442
  def _instrument(
330
443
  tracer_provider: TracerProvider = None,
444
+ meter_provider: MeterProvider = None,
331
445
  url_filter: _UrlFilterT = None,
332
446
  request_hook: _RequestHookT = None,
333
447
  response_hook: _ResponseHookT = None,
@@ -357,6 +471,7 @@ def _instrument(
357
471
  request_hook=request_hook,
358
472
  response_hook=response_hook,
359
473
  tracer_provider=tracer_provider,
474
+ meter_provider=meter_provider,
360
475
  sem_conv_opt_in_mode=sem_conv_opt_in_mode,
361
476
  )
362
477
  trace_config._is_instrumented_by_opentelemetry = True
@@ -401,6 +516,7 @@ class AioHttpClientInstrumentor(BaseInstrumentor):
401
516
  Args:
402
517
  **kwargs: Optional arguments
403
518
  ``tracer_provider``: a TracerProvider, defaults to global
519
+ ``meter_provider``: a MeterProvider, defaults to global
404
520
  ``url_filter``: A callback to process the requested URL prior to adding
405
521
  it as a span attribute. This can be useful to remove sensitive data
406
522
  such as API keys or user personal information.
@@ -415,6 +531,7 @@ class AioHttpClientInstrumentor(BaseInstrumentor):
415
531
  )
416
532
  _instrument(
417
533
  tracer_provider=kwargs.get("tracer_provider"),
534
+ meter_provider=kwargs.get("meter_provider"),
418
535
  url_filter=kwargs.get("url_filter"),
419
536
  request_hook=kwargs.get("request_hook"),
420
537
  response_hook=kwargs.get("response_hook"),
@@ -15,6 +15,6 @@
15
15
 
16
16
  _instruments = ("aiohttp ~= 3.0",)
17
17
 
18
- _supports_metrics = False
18
+ _supports_metrics = True
19
19
 
20
20
  _semconv_status = "migration"
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "0.54b0"
15
+ __version__ = "0.55b0"
@@ -12,6 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ # pylint: disable=too-many-lines
16
+
15
17
  import asyncio
16
18
  import contextlib
17
19
  import typing
@@ -28,6 +30,8 @@ from http_server_mock import HttpServerMock
28
30
  from opentelemetry import trace as trace_api
29
31
  from opentelemetry.instrumentation import aiohttp_client
30
32
  from opentelemetry.instrumentation._semconv import (
33
+ HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
34
+ HTTP_DURATION_HISTOGRAM_BUCKETS_OLD,
31
35
  OTEL_SEMCONV_STABILITY_OPT_IN,
32
36
  _OpenTelemetrySemanticConventionStability,
33
37
  _StabilityMode,
@@ -36,6 +40,20 @@ from opentelemetry.instrumentation.aiohttp_client import (
36
40
  AioHttpClientInstrumentor,
37
41
  )
38
42
  from opentelemetry.instrumentation.utils import suppress_instrumentation
43
+ from opentelemetry.semconv._incubating.attributes.http_attributes import (
44
+ HTTP_HOST,
45
+ HTTP_METHOD,
46
+ HTTP_STATUS_CODE,
47
+ HTTP_URL,
48
+ )
49
+ from opentelemetry.semconv._incubating.attributes.net_attributes import (
50
+ NET_PEER_NAME,
51
+ NET_PEER_PORT,
52
+ )
53
+ from opentelemetry.semconv._incubating.attributes.server_attributes import (
54
+ SERVER_ADDRESS,
55
+ SERVER_PORT,
56
+ )
39
57
  from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
40
58
  from opentelemetry.semconv.attributes.http_attributes import (
41
59
  HTTP_REQUEST_METHOD,
@@ -43,7 +61,6 @@ from opentelemetry.semconv.attributes.http_attributes import (
43
61
  HTTP_RESPONSE_STATUS_CODE,
44
62
  )
45
63
  from opentelemetry.semconv.attributes.url_attributes import URL_FULL
46
- from opentelemetry.semconv.trace import SpanAttributes
47
64
  from opentelemetry.test.test_base import TestBase
48
65
  from opentelemetry.trace import Span, StatusCode
49
66
  from opentelemetry.util._importlib_metadata import entry_points
@@ -84,7 +101,7 @@ class TestAioHttpIntegration(TestBase):
84
101
  super().setUp()
85
102
  _OpenTelemetrySemanticConventionStability._initialized = False
86
103
 
87
- def assert_spans(self, spans, num_spans=1):
104
+ def _assert_spans(self, spans, num_spans=1):
88
105
  finished_spans = self.memory_exporter.get_finished_spans()
89
106
  self.assertEqual(num_spans, len(finished_spans))
90
107
  self.assertEqual(
@@ -99,6 +116,11 @@ class TestAioHttpIntegration(TestBase):
99
116
  spans,
100
117
  )
101
118
 
119
+ def _assert_metrics(self, num_metrics: int = 1):
120
+ metrics = self.get_sorted_metrics()
121
+ self.assertEqual(len(metrics), num_metrics)
122
+ return metrics
123
+
102
124
  @staticmethod
103
125
  def _http_request(
104
126
  trace_config,
@@ -126,6 +148,7 @@ class TestAioHttpIntegration(TestBase):
126
148
  return run_with_test_server(client_request, url, handler)
127
149
 
128
150
  def test_status_codes(self):
151
+ index = 0
129
152
  for status_code, span_status in self._test_status_codes:
130
153
  with self.subTest(status_code=status_code):
131
154
  path = "test-path?query=param#foobar"
@@ -136,15 +159,34 @@ class TestAioHttpIntegration(TestBase):
136
159
  )
137
160
  url = f"http://{host}:{port}/{path}"
138
161
  attributes = {
139
- SpanAttributes.HTTP_METHOD: "GET",
140
- SpanAttributes.HTTP_URL: url,
141
- SpanAttributes.HTTP_STATUS_CODE: status_code,
162
+ HTTP_METHOD: "GET",
163
+ HTTP_URL: url,
164
+ HTTP_STATUS_CODE: status_code,
142
165
  }
166
+
143
167
  spans = [("GET", (span_status, None), attributes)]
144
- self.assert_spans(spans)
168
+ self._assert_spans(spans)
145
169
  self.memory_exporter.clear()
170
+ metrics = self._assert_metrics(1)
171
+ duration_data_point = metrics[0].data.data_points[index]
172
+ self.assertEqual(
173
+ dict(duration_data_point.attributes),
174
+ {
175
+ HTTP_STATUS_CODE: status_code,
176
+ HTTP_METHOD: "GET",
177
+ HTTP_HOST: host,
178
+ NET_PEER_NAME: host,
179
+ NET_PEER_PORT: port,
180
+ },
181
+ )
182
+ self.assertEqual(
183
+ duration_data_point.explicit_bounds,
184
+ HTTP_DURATION_HISTOGRAM_BUCKETS_OLD,
185
+ )
186
+ index += 1
146
187
 
147
188
  def test_status_codes_new_semconv(self):
189
+ index = 0
148
190
  for status_code, span_status in self._test_status_codes:
149
191
  with self.subTest(status_code=status_code):
150
192
  path = "test-path?query=param#foobar"
@@ -160,14 +202,39 @@ class TestAioHttpIntegration(TestBase):
160
202
  HTTP_REQUEST_METHOD: "GET",
161
203
  URL_FULL: url,
162
204
  HTTP_RESPONSE_STATUS_CODE: status_code,
205
+ SERVER_ADDRESS: host,
206
+ SERVER_PORT: port,
163
207
  }
164
208
  if status_code >= 400:
165
209
  attributes[ERROR_TYPE] = str(status_code.value)
166
210
  spans = [("GET", (span_status, None), attributes)]
167
- self.assert_spans(spans)
211
+ self._assert_spans(spans)
168
212
  self.memory_exporter.clear()
213
+ metrics = self._assert_metrics(1)
214
+ duration_data_point = metrics[0].data.data_points[index]
215
+ self.assertEqual(
216
+ duration_data_point.attributes.get(
217
+ HTTP_RESPONSE_STATUS_CODE
218
+ ),
219
+ status_code,
220
+ )
221
+ self.assertEqual(
222
+ duration_data_point.attributes.get(HTTP_REQUEST_METHOD),
223
+ "GET",
224
+ )
225
+ if status_code >= 400:
226
+ self.assertEqual(
227
+ duration_data_point.attributes.get(ERROR_TYPE),
228
+ str(status_code.value),
229
+ )
230
+ self.assertEqual(
231
+ duration_data_point.explicit_bounds,
232
+ HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
233
+ )
234
+ index += 1
169
235
 
170
236
  def test_status_codes_both_semconv(self):
237
+ index = 0
171
238
  for status_code, span_status in self._test_status_codes:
172
239
  with self.subTest(status_code=status_code):
173
240
  path = "test-path?query=param#foobar"
@@ -181,18 +248,79 @@ class TestAioHttpIntegration(TestBase):
181
248
  url = f"http://{host}:{port}/{path}"
182
249
  attributes = {
183
250
  HTTP_REQUEST_METHOD: "GET",
184
- SpanAttributes.HTTP_METHOD: "GET",
251
+ HTTP_METHOD: "GET",
252
+ HTTP_HOST: host,
185
253
  URL_FULL: url,
186
- SpanAttributes.HTTP_URL: url,
254
+ HTTP_URL: url,
187
255
  HTTP_RESPONSE_STATUS_CODE: status_code,
188
- SpanAttributes.HTTP_STATUS_CODE: status_code,
256
+ HTTP_STATUS_CODE: status_code,
257
+ SERVER_ADDRESS: host,
258
+ SERVER_PORT: port,
259
+ NET_PEER_PORT: port,
189
260
  }
261
+
190
262
  if status_code >= 400:
191
263
  attributes[ERROR_TYPE] = str(status_code.value)
192
264
 
193
265
  spans = [("GET", (span_status, None), attributes)]
194
- self.assert_spans(spans, 1)
266
+ self._assert_spans(spans, 1)
195
267
  self.memory_exporter.clear()
268
+ metrics = self._assert_metrics(2)
269
+ duration_data_point = metrics[0].data.data_points[index]
270
+ self.assertEqual(
271
+ duration_data_point.attributes.get(HTTP_STATUS_CODE),
272
+ status_code,
273
+ )
274
+ self.assertEqual(
275
+ duration_data_point.attributes.get(HTTP_METHOD),
276
+ "GET",
277
+ )
278
+ self.assertEqual(
279
+ duration_data_point.attributes.get(ERROR_TYPE),
280
+ None,
281
+ )
282
+ duration_data_point = metrics[1].data.data_points[index]
283
+ self.assertEqual(
284
+ duration_data_point.attributes.get(
285
+ HTTP_RESPONSE_STATUS_CODE
286
+ ),
287
+ status_code,
288
+ )
289
+ self.assertEqual(
290
+ duration_data_point.attributes.get(HTTP_REQUEST_METHOD),
291
+ "GET",
292
+ )
293
+ if status_code >= 400:
294
+ self.assertEqual(
295
+ duration_data_point.attributes.get(ERROR_TYPE),
296
+ str(status_code.value),
297
+ )
298
+ index += 1
299
+
300
+ def test_metrics(self):
301
+ with self.subTest(status_code=200):
302
+ host, port = self._http_request(
303
+ trace_config=aiohttp_client.create_trace_config(),
304
+ url="/test-path?query=param#foobar",
305
+ status_code=200,
306
+ )
307
+ metrics = self._assert_metrics(1)
308
+ self.assertEqual(len(metrics[0].data.data_points), 1)
309
+ duration_data_point = metrics[0].data.data_points[0]
310
+ self.assertEqual(
311
+ dict(metrics[0].data.data_points[0].attributes),
312
+ {
313
+ HTTP_STATUS_CODE: 200,
314
+ HTTP_METHOD: "GET",
315
+ HTTP_HOST: host,
316
+ NET_PEER_NAME: host,
317
+ NET_PEER_PORT: port,
318
+ },
319
+ )
320
+ self.assertEqual(duration_data_point.count, 1)
321
+ self.assertTrue(duration_data_point.min > 0)
322
+ self.assertTrue(duration_data_point.max > 0)
323
+ self.assertTrue(duration_data_point.sum > 0)
196
324
 
197
325
  def test_schema_url(self):
198
326
  with self.subTest(status_code=200):
@@ -292,16 +420,12 @@ class TestAioHttpIntegration(TestBase):
292
420
  (span.status.status_code, span.status.description),
293
421
  (StatusCode.UNSET, None),
294
422
  )
423
+ self.assertEqual(span.attributes[HTTP_METHOD], method)
295
424
  self.assertEqual(
296
- span.attributes[SpanAttributes.HTTP_METHOD], method
297
- )
298
- self.assertEqual(
299
- span.attributes[SpanAttributes.HTTP_URL],
425
+ span.attributes[HTTP_URL],
300
426
  f"http://{host}:{port}{path}",
301
427
  )
302
- self.assertEqual(
303
- span.attributes[SpanAttributes.HTTP_STATUS_CODE], HTTPStatus.OK
304
- )
428
+ self.assertEqual(span.attributes[HTTP_STATUS_CODE], HTTPStatus.OK)
305
429
  self.assertIn("response_hook_attr", span.attributes)
306
430
  self.assertEqual(span.attributes["response_hook_attr"], "value")
307
431
  self.memory_exporter.clear()
@@ -319,15 +443,15 @@ class TestAioHttpIntegration(TestBase):
319
443
  status_code=HTTPStatus.OK,
320
444
  )
321
445
 
322
- self.assert_spans(
446
+ self._assert_spans(
323
447
  [
324
448
  (
325
449
  "GET",
326
450
  (StatusCode.UNSET, None),
327
451
  {
328
- SpanAttributes.HTTP_METHOD: "GET",
329
- SpanAttributes.HTTP_URL: f"http://{host}:{port}/some/path",
330
- SpanAttributes.HTTP_STATUS_CODE: int(HTTPStatus.OK),
452
+ HTTP_METHOD: "GET",
453
+ HTTP_URL: f"http://{host}:{port}/some/path",
454
+ HTTP_STATUS_CODE: int(HTTPStatus.OK),
331
455
  },
332
456
  )
333
457
  ]
@@ -353,14 +477,14 @@ class TestAioHttpIntegration(TestBase):
353
477
  with self.assertRaises(aiohttp.ClientConnectorError):
354
478
  loop.run_until_complete(do_request(url))
355
479
 
356
- self.assert_spans(
480
+ self._assert_spans(
357
481
  [
358
482
  (
359
483
  "GET",
360
484
  (expected_status, "ClientConnectorError"),
361
485
  {
362
- SpanAttributes.HTTP_METHOD: "GET",
363
- SpanAttributes.HTTP_URL: url,
486
+ HTTP_METHOD: "GET",
487
+ HTTP_URL: url,
364
488
  },
365
489
  )
366
490
  ]
@@ -379,18 +503,29 @@ class TestAioHttpIntegration(TestBase):
379
503
  span = self.memory_exporter.get_finished_spans()[0]
380
504
  self.assertEqual(len(span.events), 1)
381
505
  self.assertEqual(span.events[0].name, "exception")
382
- self.assert_spans(
506
+ self._assert_spans(
383
507
  [
384
508
  (
385
509
  "GET",
386
510
  (StatusCode.ERROR, "ServerDisconnectedError"),
387
511
  {
388
- SpanAttributes.HTTP_METHOD: "GET",
389
- SpanAttributes.HTTP_URL: f"http://{host}:{port}/test",
512
+ HTTP_METHOD: "GET",
513
+ HTTP_URL: f"http://{host}:{port}/test",
390
514
  },
391
515
  )
392
516
  ]
393
517
  )
518
+ metrics = self._assert_metrics(1)
519
+ duration_data_point = metrics[0].data.data_points[0]
520
+ self.assertEqual(
521
+ dict(duration_data_point.attributes),
522
+ {
523
+ HTTP_METHOD: "GET",
524
+ HTTP_HOST: host,
525
+ NET_PEER_NAME: host,
526
+ NET_PEER_PORT: port,
527
+ },
528
+ )
394
529
 
395
530
  def test_basic_exception_new_semconv(self):
396
531
  async def request_handler(request):
@@ -406,7 +541,7 @@ class TestAioHttpIntegration(TestBase):
406
541
  span = self.memory_exporter.get_finished_spans()[0]
407
542
  self.assertEqual(len(span.events), 1)
408
543
  self.assertEqual(span.events[0].name, "exception")
409
- self.assert_spans(
544
+ self._assert_spans(
410
545
  [
411
546
  (
412
547
  "GET",
@@ -415,10 +550,23 @@ class TestAioHttpIntegration(TestBase):
415
550
  HTTP_REQUEST_METHOD: "GET",
416
551
  URL_FULL: f"http://{host}:{port}/test",
417
552
  ERROR_TYPE: "ServerDisconnectedError",
553
+ SERVER_ADDRESS: host,
554
+ SERVER_PORT: port,
418
555
  },
419
556
  )
420
557
  ]
421
558
  )
559
+ metrics = self._assert_metrics(1)
560
+ duration_data_point = metrics[0].data.data_points[0]
561
+ self.assertEqual(
562
+ dict(duration_data_point.attributes),
563
+ {
564
+ HTTP_REQUEST_METHOD: "GET",
565
+ ERROR_TYPE: "ServerDisconnectedError",
566
+ SERVER_ADDRESS: host,
567
+ SERVER_PORT: port,
568
+ },
569
+ )
422
570
 
423
571
  def test_basic_exception_both_semconv(self):
424
572
  async def request_handler(request):
@@ -434,7 +582,7 @@ class TestAioHttpIntegration(TestBase):
434
582
  span = self.memory_exporter.get_finished_spans()[0]
435
583
  self.assertEqual(len(span.events), 1)
436
584
  self.assertEqual(span.events[0].name, "exception")
437
- self.assert_spans(
585
+ self._assert_spans(
438
586
  [
439
587
  (
440
588
  "GET",
@@ -443,12 +591,37 @@ class TestAioHttpIntegration(TestBase):
443
591
  HTTP_REQUEST_METHOD: "GET",
444
592
  URL_FULL: f"http://{host}:{port}/test",
445
593
  ERROR_TYPE: "ServerDisconnectedError",
446
- SpanAttributes.HTTP_METHOD: "GET",
447
- SpanAttributes.HTTP_URL: f"http://{host}:{port}/test",
594
+ HTTP_METHOD: "GET",
595
+ HTTP_URL: f"http://{host}:{port}/test",
596
+ HTTP_HOST: host,
597
+ SERVER_ADDRESS: host,
598
+ SERVER_PORT: port,
599
+ NET_PEER_PORT: port,
448
600
  },
449
601
  )
450
602
  ]
451
603
  )
604
+ metrics = self._assert_metrics(2)
605
+ duration_data_point = metrics[0].data.data_points[0]
606
+ self.assertEqual(
607
+ dict(duration_data_point.attributes),
608
+ {
609
+ HTTP_METHOD: "GET",
610
+ HTTP_HOST: host,
611
+ NET_PEER_NAME: host,
612
+ NET_PEER_PORT: port,
613
+ },
614
+ )
615
+ duration_data_point = metrics[1].data.data_points[0]
616
+ self.assertEqual(
617
+ dict(duration_data_point.attributes),
618
+ {
619
+ HTTP_REQUEST_METHOD: "GET",
620
+ ERROR_TYPE: "ServerDisconnectedError",
621
+ SERVER_ADDRESS: host,
622
+ SERVER_PORT: port,
623
+ },
624
+ )
452
625
 
453
626
  def test_timeout(self):
454
627
  async def request_handler(request):
@@ -463,14 +636,14 @@ class TestAioHttpIntegration(TestBase):
463
636
  timeout=aiohttp.ClientTimeout(sock_read=0.01),
464
637
  )
465
638
 
466
- self.assert_spans(
639
+ self._assert_spans(
467
640
  [
468
641
  (
469
642
  "GET",
470
643
  (StatusCode.ERROR, "SocketTimeoutError"),
471
644
  {
472
- SpanAttributes.HTTP_METHOD: "GET",
473
- SpanAttributes.HTTP_URL: f"http://{host}:{port}/test_timeout",
645
+ HTTP_METHOD: "GET",
646
+ HTTP_URL: f"http://{host}:{port}/test_timeout",
474
647
  },
475
648
  )
476
649
  ]
@@ -490,14 +663,14 @@ class TestAioHttpIntegration(TestBase):
490
663
  max_redirects=2,
491
664
  )
492
665
 
493
- self.assert_spans(
666
+ self._assert_spans(
494
667
  [
495
668
  (
496
669
  "GET",
497
670
  (StatusCode.ERROR, "TooManyRedirects"),
498
671
  {
499
- SpanAttributes.HTTP_METHOD: "GET",
500
- SpanAttributes.HTTP_URL: f"http://{host}:{port}/test_too_many_redirects",
672
+ HTTP_METHOD: "GET",
673
+ HTTP_URL: f"http://{host}:{port}/test_too_many_redirects",
501
674
  },
502
675
  )
503
676
  ]
@@ -526,17 +699,15 @@ class TestAioHttpIntegration(TestBase):
526
699
  loop = asyncio.get_event_loop()
527
700
  loop.run_until_complete(do_request(url))
528
701
 
529
- self.assert_spans(
702
+ self._assert_spans(
530
703
  [
531
704
  (
532
705
  "HTTP",
533
706
  (StatusCode.ERROR, None),
534
707
  {
535
- SpanAttributes.HTTP_METHOD: "_OTHER",
536
- SpanAttributes.HTTP_URL: url,
537
- SpanAttributes.HTTP_STATUS_CODE: int(
538
- HTTPStatus.METHOD_NOT_ALLOWED
539
- ),
708
+ HTTP_METHOD: "_OTHER",
709
+ HTTP_URL: url,
710
+ HTTP_STATUS_CODE: int(HTTPStatus.METHOD_NOT_ALLOWED),
540
711
  },
541
712
  )
542
713
  ]
@@ -570,7 +741,7 @@ class TestAioHttpIntegration(TestBase):
570
741
  loop = asyncio.get_event_loop()
571
742
  loop.run_until_complete(do_request(url))
572
743
 
573
- self.assert_spans(
744
+ self._assert_spans(
574
745
  [
575
746
  (
576
747
  "HTTP",
@@ -583,6 +754,8 @@ class TestAioHttpIntegration(TestBase):
583
754
  ),
584
755
  HTTP_REQUEST_METHOD_ORIGINAL: "NONSTANDARD",
585
756
  ERROR_TYPE: "405",
757
+ SERVER_ADDRESS: "localhost",
758
+ SERVER_PORT: 5000,
586
759
  },
587
760
  )
588
761
  ]
@@ -613,17 +786,15 @@ class TestAioHttpIntegration(TestBase):
613
786
  loop = asyncio.get_event_loop()
614
787
  loop.run_until_complete(do_request(url))
615
788
 
616
- self.assert_spans(
789
+ self._assert_spans(
617
790
  [
618
791
  (
619
792
  "GET",
620
793
  (StatusCode.UNSET, None),
621
794
  {
622
- SpanAttributes.HTTP_METHOD: "GET",
623
- SpanAttributes.HTTP_URL: (
624
- "http://localhost:5000/status/200"
625
- ),
626
- SpanAttributes.HTTP_STATUS_CODE: int(HTTPStatus.OK),
795
+ HTTP_METHOD: "GET",
796
+ HTTP_URL: ("http://localhost:5000/status/200"),
797
+ HTTP_STATUS_CODE: int(HTTPStatus.OK),
627
798
  },
628
799
  )
629
800
  ]
@@ -656,7 +827,7 @@ class TestAioHttpClientInstrumentor(TestBase):
656
827
 
657
828
  return default_request
658
829
 
659
- def assert_spans(self, num_spans: int):
830
+ def _assert_spans(self, num_spans: int):
660
831
  finished_spans = self.memory_exporter.get_finished_spans()
661
832
  self.assertEqual(num_spans, len(finished_spans))
662
833
  if num_spans == 0:
@@ -665,18 +836,36 @@ class TestAioHttpClientInstrumentor(TestBase):
665
836
  return finished_spans[0]
666
837
  return finished_spans
667
838
 
839
+ def _assert_metrics(self, num_metrics: int = 1):
840
+ metrics = self.get_sorted_metrics()
841
+ self.assertEqual(len(metrics), num_metrics)
842
+ return metrics
843
+
668
844
  def test_instrument(self):
669
845
  host, port = run_with_test_server(
670
846
  self.get_default_request(), self.URL, self.default_handler
671
847
  )
672
- span = self.assert_spans(1)
848
+ span = self._assert_spans(1)
673
849
  self.assertEqual("GET", span.name)
674
- self.assertEqual("GET", span.attributes[SpanAttributes.HTTP_METHOD])
850
+ self.assertEqual("GET", span.attributes[HTTP_METHOD])
675
851
  self.assertEqual(
676
852
  f"http://{host}:{port}/test-path",
677
- span.attributes[SpanAttributes.HTTP_URL],
853
+ span.attributes[HTTP_URL],
854
+ )
855
+ self.assertEqual(200, span.attributes[HTTP_STATUS_CODE])
856
+ metrics = self._assert_metrics(1)
857
+ duration_data_point = metrics[0].data.data_points[0]
858
+ self.assertEqual(duration_data_point.count, 1)
859
+ self.assertEqual(
860
+ dict(duration_data_point.attributes),
861
+ {
862
+ HTTP_HOST: host,
863
+ HTTP_STATUS_CODE: 200,
864
+ HTTP_METHOD: "GET",
865
+ NET_PEER_NAME: host,
866
+ NET_PEER_PORT: port,
867
+ },
678
868
  )
679
- self.assertEqual(200, span.attributes[SpanAttributes.HTTP_STATUS_CODE])
680
869
 
681
870
  def test_instrument_new_semconv(self):
682
871
  AioHttpClientInstrumentor().uninstrument()
@@ -687,7 +876,7 @@ class TestAioHttpClientInstrumentor(TestBase):
687
876
  host, port = run_with_test_server(
688
877
  self.get_default_request(), self.URL, self.default_handler
689
878
  )
690
- span = self.assert_spans(1)
879
+ span = self._assert_spans(1)
691
880
  self.assertEqual("GET", span.name)
692
881
  self.assertEqual("GET", span.attributes[HTTP_REQUEST_METHOD])
693
882
  self.assertEqual(
@@ -695,6 +884,18 @@ class TestAioHttpClientInstrumentor(TestBase):
695
884
  span.attributes[URL_FULL],
696
885
  )
697
886
  self.assertEqual(200, span.attributes[HTTP_RESPONSE_STATUS_CODE])
887
+ metrics = self._assert_metrics(1)
888
+ duration_data_point = metrics[0].data.data_points[0]
889
+ self.assertEqual(duration_data_point.count, 1)
890
+ self.assertEqual(
891
+ dict(duration_data_point.attributes),
892
+ {
893
+ HTTP_RESPONSE_STATUS_CODE: 200,
894
+ HTTP_REQUEST_METHOD: "GET",
895
+ SERVER_ADDRESS: host,
896
+ SERVER_PORT: port,
897
+ },
898
+ )
698
899
 
699
900
  def test_instrument_both_semconv(self):
700
901
  AioHttpClientInstrumentor().uninstrument()
@@ -706,17 +907,47 @@ class TestAioHttpClientInstrumentor(TestBase):
706
907
  self.get_default_request(), self.URL, self.default_handler
707
908
  )
708
909
  url = f"http://{host}:{port}/test-path"
709
- attributes = {
710
- HTTP_REQUEST_METHOD: "GET",
711
- SpanAttributes.HTTP_METHOD: "GET",
712
- URL_FULL: url,
713
- SpanAttributes.HTTP_URL: url,
714
- HTTP_RESPONSE_STATUS_CODE: 200,
715
- SpanAttributes.HTTP_STATUS_CODE: 200,
716
- }
717
- span = self.assert_spans(1)
910
+ span = self._assert_spans(1)
718
911
  self.assertEqual("GET", span.name)
719
- self.assertEqual(span.attributes, attributes)
912
+ self.assertEqual(
913
+ dict(span.attributes),
914
+ {
915
+ HTTP_REQUEST_METHOD: "GET",
916
+ HTTP_METHOD: "GET",
917
+ HTTP_HOST: host,
918
+ URL_FULL: url,
919
+ HTTP_URL: url,
920
+ HTTP_RESPONSE_STATUS_CODE: 200,
921
+ HTTP_STATUS_CODE: 200,
922
+ SERVER_ADDRESS: host,
923
+ SERVER_PORT: port,
924
+ NET_PEER_PORT: port,
925
+ },
926
+ )
927
+ metrics = self._assert_metrics(2)
928
+ duration_data_point = metrics[0].data.data_points[0]
929
+ self.assertEqual(duration_data_point.count, 1)
930
+ self.assertEqual(
931
+ dict(duration_data_point.attributes),
932
+ {
933
+ HTTP_STATUS_CODE: 200,
934
+ HTTP_METHOD: "GET",
935
+ HTTP_HOST: host,
936
+ NET_PEER_NAME: host,
937
+ NET_PEER_PORT: port,
938
+ },
939
+ )
940
+ duration_data_point = metrics[1].data.data_points[0]
941
+ self.assertEqual(duration_data_point.count, 1)
942
+ self.assertEqual(
943
+ dict(duration_data_point.attributes),
944
+ {
945
+ HTTP_RESPONSE_STATUS_CODE: 200,
946
+ HTTP_REQUEST_METHOD: "GET",
947
+ SERVER_ADDRESS: host,
948
+ SERVER_PORT: port,
949
+ },
950
+ )
720
951
 
721
952
  def test_instrument_with_custom_trace_config(self):
722
953
  trace_config = aiohttp.TraceConfig()
@@ -733,7 +964,7 @@ class TestAioHttpClientInstrumentor(TestBase):
733
964
  await session.get(TestAioHttpClientInstrumentor.URL)
734
965
 
735
966
  run_with_test_server(make_request, self.URL, self.default_handler)
736
- self.assert_spans(1)
967
+ self._assert_spans(1)
737
968
 
738
969
  def test_every_request_by_new_session_creates_one_span(self):
739
970
  async def make_request(server: aiohttp.test_utils.TestServer):
@@ -747,7 +978,7 @@ class TestAioHttpClientInstrumentor(TestBase):
747
978
  run_with_test_server(
748
979
  make_request, self.URL, self.default_handler
749
980
  )
750
- self.assert_spans(1)
981
+ self._assert_spans(1)
751
982
 
752
983
  def test_instrument_with_existing_trace_config(self):
753
984
  trace_config = aiohttp.TraceConfig()
@@ -764,7 +995,7 @@ class TestAioHttpClientInstrumentor(TestBase):
764
995
  await session.get(TestAioHttpClientInstrumentor.URL)
765
996
 
766
997
  run_with_test_server(create_session, self.URL, self.default_handler)
767
- self.assert_spans(1)
998
+ self._assert_spans(1)
768
999
 
769
1000
  def test_no_op_tracer_provider(self):
770
1001
  AioHttpClientInstrumentor().uninstrument()
@@ -784,13 +1015,13 @@ class TestAioHttpClientInstrumentor(TestBase):
784
1015
  self.get_default_request(), self.URL, self.default_handler
785
1016
  )
786
1017
 
787
- self.assert_spans(0)
1018
+ self._assert_spans(0)
788
1019
 
789
1020
  AioHttpClientInstrumentor().instrument()
790
1021
  run_with_test_server(
791
1022
  self.get_default_request(), self.URL, self.default_handler
792
1023
  )
793
- self.assert_spans(1)
1024
+ self._assert_spans(1)
794
1025
 
795
1026
  def test_uninstrument_session(self):
796
1027
  async def uninstrument_request(server: aiohttp.test_utils.TestServer):
@@ -802,19 +1033,19 @@ class TestAioHttpClientInstrumentor(TestBase):
802
1033
  run_with_test_server(
803
1034
  uninstrument_request, self.URL, self.default_handler
804
1035
  )
805
- self.assert_spans(0)
1036
+ self._assert_spans(0)
806
1037
 
807
1038
  run_with_test_server(
808
1039
  self.get_default_request(), self.URL, self.default_handler
809
1040
  )
810
- self.assert_spans(1)
1041
+ self._assert_spans(1)
811
1042
 
812
1043
  def test_suppress_instrumentation(self):
813
1044
  with suppress_instrumentation():
814
1045
  run_with_test_server(
815
1046
  self.get_default_request(), self.URL, self.default_handler
816
1047
  )
817
- self.assert_spans(0)
1048
+ self._assert_spans(0)
818
1049
 
819
1050
  @staticmethod
820
1051
  async def suppressed_request(server: aiohttp.test_utils.TestServer):
@@ -826,7 +1057,7 @@ class TestAioHttpClientInstrumentor(TestBase):
826
1057
  run_with_test_server(
827
1058
  self.suppressed_request, self.URL, self.default_handler
828
1059
  )
829
- self.assert_spans(0)
1060
+ self._assert_spans(0)
830
1061
 
831
1062
  def test_suppress_instrumentation_with_server_exception(self):
832
1063
  # pylint:disable=unused-argument
@@ -836,7 +1067,7 @@ class TestAioHttpClientInstrumentor(TestBase):
836
1067
  run_with_test_server(
837
1068
  self.suppressed_request, self.URL, raising_handler
838
1069
  )
839
- self.assert_spans(0)
1070
+ self._assert_spans(0)
840
1071
 
841
1072
  def test_url_filter(self):
842
1073
  def strip_query_params(url: yarl.URL) -> str:
@@ -849,10 +1080,10 @@ class TestAioHttpClientInstrumentor(TestBase):
849
1080
  host, port = run_with_test_server(
850
1081
  self.get_default_request(url), url, self.default_handler
851
1082
  )
852
- span = self.assert_spans(1)
1083
+ span = self._assert_spans(1)
853
1084
  self.assertEqual(
854
1085
  f"http://{host}:{port}/test-path",
855
- span.attributes[SpanAttributes.HTTP_URL],
1086
+ span.attributes[HTTP_URL],
856
1087
  )
857
1088
 
858
1089
  def test_hooks(self):
@@ -877,7 +1108,7 @@ class TestAioHttpClientInstrumentor(TestBase):
877
1108
  run_with_test_server(
878
1109
  self.get_default_request(url), url, self.default_handler
879
1110
  )
880
- span = self.assert_spans(1)
1111
+ span = self._assert_spans(1)
881
1112
  self.assertEqual("GET - /test-path", span.name)
882
1113
  self.assertIn("response_hook_attr", span.attributes)
883
1114
  self.assertEqual(span.attributes["response_hook_attr"], "value")