opentelemetry-instrumentation-aiohttp-client 0.46b0__tar.gz → 0.47b0__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.
@@ -58,3 +58,6 @@ _build/
58
58
  # mypy
59
59
  .mypy_cache/
60
60
  target
61
+
62
+ # Benchmark result files
63
+ *-benchmark.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: opentelemetry-instrumentation-aiohttp-client
3
- Version: 0.46b0
3
+ Version: 0.47b0
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
  Author-email: OpenTelemetry Authors <cncf-opentelemetry-contributors@lists.cncf.io>
@@ -15,11 +15,12 @@ Classifier: Programming Language :: Python :: 3.8
15
15
  Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
18
19
  Requires-Python: >=3.8
19
20
  Requires-Dist: opentelemetry-api~=1.12
20
- Requires-Dist: opentelemetry-instrumentation==0.46b0
21
- Requires-Dist: opentelemetry-semantic-conventions==0.46b0
22
- Requires-Dist: opentelemetry-util-http==0.46b0
21
+ Requires-Dist: opentelemetry-instrumentation==0.47b0
22
+ Requires-Dist: opentelemetry-semantic-conventions==0.47b0
23
+ Requires-Dist: opentelemetry-util-http==0.47b0
23
24
  Requires-Dist: wrapt<2.0.0,>=1.0.0
24
25
  Provides-Extra: instruments
25
26
  Requires-Dist: aiohttp~=3.0; extra == 'instruments'
@@ -22,12 +22,13 @@ classifiers = [
22
22
  "Programming Language :: Python :: 3.9",
23
23
  "Programming Language :: Python :: 3.10",
24
24
  "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
25
26
  ]
26
27
  dependencies = [
27
28
  "opentelemetry-api ~= 1.12",
28
- "opentelemetry-instrumentation == 0.46b0",
29
- "opentelemetry-semantic-conventions == 0.46b0",
30
- "opentelemetry-util-http == 0.46b0",
29
+ "opentelemetry-instrumentation == 0.47b0",
30
+ "opentelemetry-semantic-conventions == 0.47b0",
31
+ "opentelemetry-util-http == 0.47b0",
31
32
  "wrapt >= 1.0.0, < 2.0.0",
32
33
  ]
33
34
 
@@ -90,19 +90,28 @@ import yarl
90
90
 
91
91
  from opentelemetry import context as context_api
92
92
  from opentelemetry import trace
93
+ from opentelemetry.instrumentation._semconv import (
94
+ _get_schema_url,
95
+ _HTTPStabilityMode,
96
+ _OpenTelemetrySemanticConventionStability,
97
+ _OpenTelemetryStabilitySignalType,
98
+ _report_new,
99
+ _set_http_method,
100
+ _set_http_url,
101
+ _set_status,
102
+ )
93
103
  from opentelemetry.instrumentation.aiohttp_client.package import _instruments
94
104
  from opentelemetry.instrumentation.aiohttp_client.version import __version__
95
105
  from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
96
106
  from opentelemetry.instrumentation.utils import (
97
- http_status_to_status_code,
98
107
  is_instrumentation_enabled,
99
108
  unwrap,
100
109
  )
101
110
  from opentelemetry.propagate import inject
102
- from opentelemetry.semconv.trace import SpanAttributes
111
+ from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
103
112
  from opentelemetry.trace import Span, SpanKind, TracerProvider, get_tracer
104
113
  from opentelemetry.trace.status import Status, StatusCode
105
- from opentelemetry.util.http import remove_url_credentials
114
+ from opentelemetry.util.http import remove_url_credentials, sanitize_method
106
115
 
107
116
  _UrlFilterT = typing.Optional[typing.Callable[[yarl.URL], str]]
108
117
  _RequestHookT = typing.Optional[
@@ -122,11 +131,45 @@ _ResponseHookT = typing.Optional[
122
131
  ]
123
132
 
124
133
 
134
+ def _get_span_name(method: str) -> str:
135
+ method = sanitize_method(method.strip())
136
+ if method == "_OTHER":
137
+ method = "HTTP"
138
+ return method
139
+
140
+
141
+ def _set_http_status_code_attribute(
142
+ span,
143
+ status_code,
144
+ metric_attributes=None,
145
+ sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT,
146
+ ):
147
+ status_code_str = str(status_code)
148
+ try:
149
+ status_code = int(status_code)
150
+ except ValueError:
151
+ status_code = -1
152
+ if metric_attributes is None:
153
+ metric_attributes = {}
154
+ # When we have durations we should set metrics only once
155
+ # Also the decision to include status code on a histogram should
156
+ # not be dependent on tracing decisions.
157
+ _set_status(
158
+ span,
159
+ metric_attributes,
160
+ status_code,
161
+ status_code_str,
162
+ server_span=False,
163
+ sem_conv_opt_in_mode=sem_conv_opt_in_mode,
164
+ )
165
+
166
+
125
167
  def create_trace_config(
126
168
  url_filter: _UrlFilterT = None,
127
169
  request_hook: _RequestHookT = None,
128
170
  response_hook: _ResponseHookT = None,
129
171
  tracer_provider: TracerProvider = None,
172
+ sem_conv_opt_in_mode: _HTTPStabilityMode = _HTTPStabilityMode.DEFAULT,
130
173
  ) -> aiohttp.TraceConfig:
131
174
  """Create an aiohttp-compatible trace configuration.
132
175
 
@@ -167,9 +210,12 @@ def create_trace_config(
167
210
  __name__,
168
211
  __version__,
169
212
  tracer_provider,
170
- schema_url="https://opentelemetry.io/schemas/1.11.0",
213
+ schema_url=_get_schema_url(sem_conv_opt_in_mode),
171
214
  )
172
215
 
216
+ # TODO: Use this when we have durations for aiohttp-client
217
+ metric_attributes = {}
218
+
173
219
  def _end_trace(trace_config_ctx: types.SimpleNamespace):
174
220
  context_api.detach(trace_config_ctx.token)
175
221
  trace_config_ctx.span.end()
@@ -183,18 +229,22 @@ def create_trace_config(
183
229
  trace_config_ctx.span = None
184
230
  return
185
231
 
186
- http_method = params.method.upper()
187
- request_span_name = f"{http_method}"
232
+ method = params.method
233
+ request_span_name = _get_span_name(method)
188
234
  request_url = (
189
235
  remove_url_credentials(trace_config_ctx.url_filter(params.url))
190
236
  if callable(trace_config_ctx.url_filter)
191
237
  else remove_url_credentials(str(params.url))
192
238
  )
193
239
 
194
- span_attributes = {
195
- SpanAttributes.HTTP_METHOD: http_method,
196
- SpanAttributes.HTTP_URL: request_url,
197
- }
240
+ span_attributes = {}
241
+ _set_http_method(
242
+ span_attributes,
243
+ method,
244
+ sanitize_method(method),
245
+ sem_conv_opt_in_mode,
246
+ )
247
+ _set_http_url(span_attributes, request_url, sem_conv_opt_in_mode)
198
248
 
199
249
  trace_config_ctx.span = trace_config_ctx.tracer.start_span(
200
250
  request_span_name, kind=SpanKind.CLIENT, attributes=span_attributes
@@ -219,14 +269,13 @@ def create_trace_config(
219
269
 
220
270
  if callable(response_hook):
221
271
  response_hook(trace_config_ctx.span, params)
272
+ _set_http_status_code_attribute(
273
+ trace_config_ctx.span,
274
+ params.response.status,
275
+ metric_attributes,
276
+ sem_conv_opt_in_mode,
277
+ )
222
278
 
223
- if trace_config_ctx.span.is_recording():
224
- trace_config_ctx.span.set_status(
225
- Status(http_status_to_status_code(int(params.response.status)))
226
- )
227
- trace_config_ctx.span.set_attribute(
228
- SpanAttributes.HTTP_STATUS_CODE, params.response.status
229
- )
230
279
  _end_trace(trace_config_ctx)
231
280
 
232
281
  async def on_request_exception(
@@ -238,7 +287,13 @@ def create_trace_config(
238
287
  return
239
288
 
240
289
  if trace_config_ctx.span.is_recording() and params.exception:
241
- trace_config_ctx.span.set_status(Status(StatusCode.ERROR))
290
+ exc_type = type(params.exception).__qualname__
291
+ if _report_new(sem_conv_opt_in_mode):
292
+ trace_config_ctx.span.set_attribute(ERROR_TYPE, exc_type)
293
+
294
+ trace_config_ctx.span.set_status(
295
+ Status(StatusCode.ERROR, exc_type)
296
+ )
242
297
  trace_config_ctx.span.record_exception(params.exception)
243
298
 
244
299
  if callable(response_hook):
@@ -271,6 +326,7 @@ def _instrument(
271
326
  trace_configs: typing.Optional[
272
327
  typing.Sequence[aiohttp.TraceConfig]
273
328
  ] = None,
329
+ sem_conv_opt_in_mode: _HTTPStabilityMode = _HTTPStabilityMode.DEFAULT,
274
330
  ):
275
331
  """Enables tracing of all ClientSessions
276
332
 
@@ -293,6 +349,7 @@ def _instrument(
293
349
  request_hook=request_hook,
294
350
  response_hook=response_hook,
295
351
  tracer_provider=tracer_provider,
352
+ sem_conv_opt_in_mode=sem_conv_opt_in_mode,
296
353
  )
297
354
  trace_config._is_instrumented_by_opentelemetry = True
298
355
  client_trace_configs.append(trace_config)
@@ -344,12 +401,17 @@ class AioHttpClientInstrumentor(BaseInstrumentor):
344
401
  ``trace_configs``: An optional list of aiohttp.TraceConfig items, allowing customize enrichment of spans
345
402
  based on aiohttp events (see specification: https://docs.aiohttp.org/en/stable/tracing_reference.html)
346
403
  """
404
+ _OpenTelemetrySemanticConventionStability._initialize()
405
+ _sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
406
+ _OpenTelemetryStabilitySignalType.HTTP,
407
+ )
347
408
  _instrument(
348
409
  tracer_provider=kwargs.get("tracer_provider"),
349
410
  url_filter=kwargs.get("url_filter"),
350
411
  request_hook=kwargs.get("request_hook"),
351
412
  response_hook=kwargs.get("response_hook"),
352
413
  trace_configs=kwargs.get("trace_configs"),
414
+ sem_conv_opt_in_mode=_sem_conv_opt_in_mode,
353
415
  )
354
416
 
355
417
  def _uninstrument(self, **kwargs):
@@ -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.46b0"
15
+ __version__ = "0.47b0"
@@ -14,7 +14,6 @@
14
14
 
15
15
  import asyncio
16
16
  import contextlib
17
- import sys
18
17
  import typing
19
18
  import unittest
20
19
  import urllib.parse
@@ -29,10 +28,22 @@ from pkg_resources import iter_entry_points
29
28
 
30
29
  from opentelemetry import trace as trace_api
31
30
  from opentelemetry.instrumentation import aiohttp_client
31
+ from opentelemetry.instrumentation._semconv import (
32
+ OTEL_SEMCONV_STABILITY_OPT_IN,
33
+ _HTTPStabilityMode,
34
+ _OpenTelemetrySemanticConventionStability,
35
+ )
32
36
  from opentelemetry.instrumentation.aiohttp_client import (
33
37
  AioHttpClientInstrumentor,
34
38
  )
35
39
  from opentelemetry.instrumentation.utils import suppress_instrumentation
40
+ from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
41
+ from opentelemetry.semconv.attributes.http_attributes import (
42
+ HTTP_REQUEST_METHOD,
43
+ HTTP_REQUEST_METHOD_ORIGINAL,
44
+ HTTP_RESPONSE_STATUS_CODE,
45
+ )
46
+ from opentelemetry.semconv.attributes.url_attributes import URL_FULL
36
47
  from opentelemetry.semconv.trace import SpanAttributes
37
48
  from opentelemetry.test.test_base import TestBase
38
49
  from opentelemetry.trace import Span, StatusCode
@@ -60,7 +71,23 @@ def run_with_test_server(
60
71
 
61
72
 
62
73
  class TestAioHttpIntegration(TestBase):
63
- def assert_spans(self, spans):
74
+
75
+ _test_status_codes = (
76
+ (HTTPStatus.OK, StatusCode.UNSET),
77
+ (HTTPStatus.TEMPORARY_REDIRECT, StatusCode.UNSET),
78
+ (HTTPStatus.NOT_FOUND, StatusCode.ERROR),
79
+ (HTTPStatus.BAD_REQUEST, StatusCode.ERROR),
80
+ (HTTPStatus.SERVICE_UNAVAILABLE, StatusCode.ERROR),
81
+ (HTTPStatus.GATEWAY_TIMEOUT, StatusCode.ERROR),
82
+ )
83
+
84
+ def setUp(self):
85
+ super().setUp()
86
+ _OpenTelemetrySemanticConventionStability._initialized = False
87
+
88
+ def assert_spans(self, spans, num_spans=1):
89
+ finished_spans = self.memory_exporter.get_finished_spans()
90
+ self.assertEqual(num_spans, len(finished_spans))
64
91
  self.assertEqual(
65
92
  [
66
93
  (
@@ -68,7 +95,7 @@ class TestAioHttpIntegration(TestBase):
68
95
  (span.status.status_code, span.status.description),
69
96
  dict(span.attributes),
70
97
  )
71
- for span in self.memory_exporter.get_finished_spans()
98
+ for span in finished_spans
72
99
  ],
73
100
  spans,
74
101
  )
@@ -100,43 +127,72 @@ class TestAioHttpIntegration(TestBase):
100
127
  return run_with_test_server(client_request, url, handler)
101
128
 
102
129
  def test_status_codes(self):
103
- for status_code, span_status in (
104
- (HTTPStatus.OK, StatusCode.UNSET),
105
- (HTTPStatus.TEMPORARY_REDIRECT, StatusCode.UNSET),
106
- (HTTPStatus.SERVICE_UNAVAILABLE, StatusCode.ERROR),
107
- (
108
- HTTPStatus.GATEWAY_TIMEOUT,
109
- StatusCode.ERROR,
110
- ),
111
- ):
130
+ for status_code, span_status in self._test_status_codes:
112
131
  with self.subTest(status_code=status_code):
132
+ path = "test-path?query=param#foobar"
113
133
  host, port = self._http_request(
114
134
  trace_config=aiohttp_client.create_trace_config(),
115
- url="/test-path?query=param#foobar",
135
+ url=f"/{path}",
116
136
  status_code=status_code,
117
137
  )
138
+ url = f"http://{host}:{port}/{path}"
139
+ attributes = {
140
+ SpanAttributes.HTTP_METHOD: "GET",
141
+ SpanAttributes.HTTP_URL: url,
142
+ SpanAttributes.HTTP_STATUS_CODE: status_code,
143
+ }
144
+ spans = [("GET", (span_status, None), attributes)]
145
+ self.assert_spans(spans)
146
+ self.memory_exporter.clear()
118
147
 
119
- url = f"http://{host}:{port}/test-path?query=param#foobar"
120
- # if python version is < 3.8, then the url will be
121
- if sys.version_info[1] < 8:
122
- url = f"http://{host}:{port}/test-path#foobar"
123
-
124
- self.assert_spans(
125
- [
126
- (
127
- "GET",
128
- (span_status, None),
129
- {
130
- SpanAttributes.HTTP_METHOD: "GET",
131
- SpanAttributes.HTTP_URL: url,
132
- SpanAttributes.HTTP_STATUS_CODE: int(
133
- status_code
134
- ),
135
- },
136
- )
137
- ]
148
+ def test_status_codes_new_semconv(self):
149
+ for status_code, span_status in self._test_status_codes:
150
+ with self.subTest(status_code=status_code):
151
+ path = "test-path?query=param#foobar"
152
+ host, port = self._http_request(
153
+ trace_config=aiohttp_client.create_trace_config(
154
+ sem_conv_opt_in_mode=_HTTPStabilityMode.HTTP
155
+ ),
156
+ url=f"/{path}",
157
+ status_code=status_code,
138
158
  )
159
+ url = f"http://{host}:{port}/{path}"
160
+ attributes = {
161
+ HTTP_REQUEST_METHOD: "GET",
162
+ URL_FULL: url,
163
+ HTTP_RESPONSE_STATUS_CODE: status_code,
164
+ }
165
+ if status_code >= 400:
166
+ attributes[ERROR_TYPE] = str(status_code.value)
167
+ spans = [("GET", (span_status, None), attributes)]
168
+ self.assert_spans(spans)
169
+ self.memory_exporter.clear()
139
170
 
171
+ def test_status_codes_both_semconv(self):
172
+ for status_code, span_status in self._test_status_codes:
173
+ with self.subTest(status_code=status_code):
174
+ path = "test-path?query=param#foobar"
175
+ host, port = self._http_request(
176
+ trace_config=aiohttp_client.create_trace_config(
177
+ sem_conv_opt_in_mode=_HTTPStabilityMode.HTTP_DUP
178
+ ),
179
+ url=f"/{path}",
180
+ status_code=status_code,
181
+ )
182
+ url = f"http://{host}:{port}/{path}"
183
+ attributes = {
184
+ HTTP_REQUEST_METHOD: "GET",
185
+ SpanAttributes.HTTP_METHOD: "GET",
186
+ URL_FULL: url,
187
+ SpanAttributes.HTTP_URL: url,
188
+ HTTP_RESPONSE_STATUS_CODE: status_code,
189
+ SpanAttributes.HTTP_STATUS_CODE: status_code,
190
+ }
191
+ if status_code >= 400:
192
+ attributes[ERROR_TYPE] = str(status_code.value)
193
+
194
+ spans = [("GET", (span_status, None), attributes)]
195
+ self.assert_spans(spans, 1)
140
196
  self.memory_exporter.clear()
141
197
 
142
198
  def test_schema_url(self):
@@ -154,6 +210,40 @@ class TestAioHttpIntegration(TestBase):
154
210
  )
155
211
  self.memory_exporter.clear()
156
212
 
213
+ def test_schema_url_new_semconv(self):
214
+ with self.subTest(status_code=200):
215
+ self._http_request(
216
+ trace_config=aiohttp_client.create_trace_config(
217
+ sem_conv_opt_in_mode=_HTTPStabilityMode.HTTP
218
+ ),
219
+ url="/test-path?query=param#foobar",
220
+ status_code=200,
221
+ )
222
+
223
+ span = self.memory_exporter.get_finished_spans()[0]
224
+ self.assertEqual(
225
+ span.instrumentation_info.schema_url,
226
+ "https://opentelemetry.io/schemas/1.21.0",
227
+ )
228
+ self.memory_exporter.clear()
229
+
230
+ def test_schema_url_both_semconv(self):
231
+ with self.subTest(status_code=200):
232
+ self._http_request(
233
+ trace_config=aiohttp_client.create_trace_config(
234
+ sem_conv_opt_in_mode=_HTTPStabilityMode.HTTP_DUP
235
+ ),
236
+ url="/test-path?query=param#foobar",
237
+ status_code=200,
238
+ )
239
+
240
+ span = self.memory_exporter.get_finished_spans()[0]
241
+ self.assertEqual(
242
+ span.instrumentation_info.schema_url,
243
+ "https://opentelemetry.io/schemas/1.21.0",
244
+ )
245
+ self.memory_exporter.clear()
246
+
157
247
  def test_not_recording(self):
158
248
  mock_tracer = mock.Mock()
159
249
  mock_span = mock.Mock()
@@ -268,7 +358,7 @@ class TestAioHttpIntegration(TestBase):
268
358
  [
269
359
  (
270
360
  "GET",
271
- (expected_status, None),
361
+ (expected_status, "ClientConnectorError"),
272
362
  {
273
363
  SpanAttributes.HTTP_METHOD: "GET",
274
364
  SpanAttributes.HTTP_URL: url,
@@ -278,6 +368,89 @@ class TestAioHttpIntegration(TestBase):
278
368
  )
279
369
  self.memory_exporter.clear()
280
370
 
371
+ def test_basic_exception(self):
372
+ async def request_handler(request):
373
+ assert "traceparent" in request.headers
374
+
375
+ host, port = self._http_request(
376
+ trace_config=aiohttp_client.create_trace_config(),
377
+ url="/test",
378
+ request_handler=request_handler,
379
+ )
380
+ span = self.memory_exporter.get_finished_spans()[0]
381
+ self.assertEqual(len(span.events), 1)
382
+ self.assertEqual(span.events[0].name, "exception")
383
+ self.assert_spans(
384
+ [
385
+ (
386
+ "GET",
387
+ (StatusCode.ERROR, "ServerDisconnectedError"),
388
+ {
389
+ SpanAttributes.HTTP_METHOD: "GET",
390
+ SpanAttributes.HTTP_URL: f"http://{host}:{port}/test",
391
+ },
392
+ )
393
+ ]
394
+ )
395
+
396
+ def test_basic_exception_new_semconv(self):
397
+ async def request_handler(request):
398
+ assert "traceparent" in request.headers
399
+
400
+ host, port = self._http_request(
401
+ trace_config=aiohttp_client.create_trace_config(
402
+ sem_conv_opt_in_mode=_HTTPStabilityMode.HTTP
403
+ ),
404
+ url="/test",
405
+ request_handler=request_handler,
406
+ )
407
+ span = self.memory_exporter.get_finished_spans()[0]
408
+ self.assertEqual(len(span.events), 1)
409
+ self.assertEqual(span.events[0].name, "exception")
410
+ self.assert_spans(
411
+ [
412
+ (
413
+ "GET",
414
+ (StatusCode.ERROR, "ServerDisconnectedError"),
415
+ {
416
+ HTTP_REQUEST_METHOD: "GET",
417
+ URL_FULL: f"http://{host}:{port}/test",
418
+ ERROR_TYPE: "ServerDisconnectedError",
419
+ },
420
+ )
421
+ ]
422
+ )
423
+
424
+ def test_basic_exception_both_semconv(self):
425
+ async def request_handler(request):
426
+ assert "traceparent" in request.headers
427
+
428
+ host, port = self._http_request(
429
+ trace_config=aiohttp_client.create_trace_config(
430
+ sem_conv_opt_in_mode=_HTTPStabilityMode.HTTP_DUP
431
+ ),
432
+ url="/test",
433
+ request_handler=request_handler,
434
+ )
435
+ span = self.memory_exporter.get_finished_spans()[0]
436
+ self.assertEqual(len(span.events), 1)
437
+ self.assertEqual(span.events[0].name, "exception")
438
+ self.assert_spans(
439
+ [
440
+ (
441
+ "GET",
442
+ (StatusCode.ERROR, "ServerDisconnectedError"),
443
+ {
444
+ HTTP_REQUEST_METHOD: "GET",
445
+ URL_FULL: f"http://{host}:{port}/test",
446
+ ERROR_TYPE: "ServerDisconnectedError",
447
+ SpanAttributes.HTTP_METHOD: "GET",
448
+ SpanAttributes.HTTP_URL: f"http://{host}:{port}/test",
449
+ },
450
+ )
451
+ ]
452
+ )
453
+
281
454
  def test_timeout(self):
282
455
  async def request_handler(request):
283
456
  await asyncio.sleep(1)
@@ -295,7 +468,7 @@ class TestAioHttpIntegration(TestBase):
295
468
  [
296
469
  (
297
470
  "GET",
298
- (StatusCode.ERROR, None),
471
+ (StatusCode.ERROR, "ServerTimeoutError"),
299
472
  {
300
473
  SpanAttributes.HTTP_METHOD: "GET",
301
474
  SpanAttributes.HTTP_URL: f"http://{host}:{port}/test_timeout",
@@ -322,7 +495,7 @@ class TestAioHttpIntegration(TestBase):
322
495
  [
323
496
  (
324
497
  "GET",
325
- (StatusCode.ERROR, None),
498
+ (StatusCode.ERROR, "TooManyRedirects"),
326
499
  {
327
500
  SpanAttributes.HTTP_METHOD: "GET",
328
501
  SpanAttributes.HTTP_URL: f"http://{host}:{port}/test_too_many_redirects",
@@ -331,6 +504,92 @@ class TestAioHttpIntegration(TestBase):
331
504
  ]
332
505
  )
333
506
 
507
+ def test_nonstandard_http_method(self):
508
+ trace_configs = [aiohttp_client.create_trace_config()]
509
+ app = HttpServerMock("nonstandard_method")
510
+
511
+ @app.route("/status/200", methods=["NONSTANDARD"])
512
+ def index():
513
+ return ("", 405, {})
514
+
515
+ url = "http://localhost:5000/status/200"
516
+
517
+ with app.run("localhost", 5000):
518
+ with self.subTest(url=url):
519
+
520
+ async def do_request(url):
521
+ async with aiohttp.ClientSession(
522
+ trace_configs=trace_configs,
523
+ ) as session:
524
+ async with session.request("NONSTANDARD", url):
525
+ pass
526
+
527
+ loop = asyncio.get_event_loop()
528
+ loop.run_until_complete(do_request(url))
529
+
530
+ self.assert_spans(
531
+ [
532
+ (
533
+ "HTTP",
534
+ (StatusCode.ERROR, None),
535
+ {
536
+ SpanAttributes.HTTP_METHOD: "_OTHER",
537
+ SpanAttributes.HTTP_URL: url,
538
+ SpanAttributes.HTTP_STATUS_CODE: int(
539
+ HTTPStatus.METHOD_NOT_ALLOWED
540
+ ),
541
+ },
542
+ )
543
+ ]
544
+ )
545
+ self.memory_exporter.clear()
546
+
547
+ def test_nonstandard_http_method_new_semconv(self):
548
+ trace_configs = [
549
+ aiohttp_client.create_trace_config(
550
+ sem_conv_opt_in_mode=_HTTPStabilityMode.HTTP
551
+ )
552
+ ]
553
+ app = HttpServerMock("nonstandard_method")
554
+
555
+ @app.route("/status/200", methods=["NONSTANDARD"])
556
+ def index():
557
+ return ("", 405, {})
558
+
559
+ url = "http://localhost:5000/status/200"
560
+
561
+ with app.run("localhost", 5000):
562
+ with self.subTest(url=url):
563
+
564
+ async def do_request(url):
565
+ async with aiohttp.ClientSession(
566
+ trace_configs=trace_configs,
567
+ ) as session:
568
+ async with session.request("NONSTANDARD", url):
569
+ pass
570
+
571
+ loop = asyncio.get_event_loop()
572
+ loop.run_until_complete(do_request(url))
573
+
574
+ self.assert_spans(
575
+ [
576
+ (
577
+ "HTTP",
578
+ (StatusCode.ERROR, None),
579
+ {
580
+ HTTP_REQUEST_METHOD: "_OTHER",
581
+ URL_FULL: url,
582
+ HTTP_RESPONSE_STATUS_CODE: int(
583
+ HTTPStatus.METHOD_NOT_ALLOWED
584
+ ),
585
+ HTTP_REQUEST_METHOD_ORIGINAL: "NONSTANDARD",
586
+ ERROR_TYPE: "405",
587
+ },
588
+ )
589
+ ]
590
+ )
591
+ self.memory_exporter.clear()
592
+
334
593
  def test_credential_removal(self):
335
594
  trace_configs = [aiohttp_client.create_trace_config()]
336
595
 
@@ -379,6 +638,7 @@ class TestAioHttpClientInstrumentor(TestBase):
379
638
  def setUp(self):
380
639
  super().setUp()
381
640
  AioHttpClientInstrumentor().instrument()
641
+ _OpenTelemetrySemanticConventionStability._initialized = False
382
642
 
383
643
  def tearDown(self):
384
644
  super().tearDown()
@@ -419,6 +679,46 @@ class TestAioHttpClientInstrumentor(TestBase):
419
679
  )
420
680
  self.assertEqual(200, span.attributes[SpanAttributes.HTTP_STATUS_CODE])
421
681
 
682
+ def test_instrument_new_semconv(self):
683
+ AioHttpClientInstrumentor().uninstrument()
684
+ with mock.patch.dict(
685
+ "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "http"}
686
+ ):
687
+ AioHttpClientInstrumentor().instrument()
688
+ host, port = run_with_test_server(
689
+ self.get_default_request(), self.URL, self.default_handler
690
+ )
691
+ span = self.assert_spans(1)
692
+ self.assertEqual("GET", span.name)
693
+ self.assertEqual("GET", span.attributes[HTTP_REQUEST_METHOD])
694
+ self.assertEqual(
695
+ f"http://{host}:{port}/test-path",
696
+ span.attributes[URL_FULL],
697
+ )
698
+ self.assertEqual(200, span.attributes[HTTP_RESPONSE_STATUS_CODE])
699
+
700
+ def test_instrument_both_semconv(self):
701
+ AioHttpClientInstrumentor().uninstrument()
702
+ with mock.patch.dict(
703
+ "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "http/dup"}
704
+ ):
705
+ AioHttpClientInstrumentor().instrument()
706
+ host, port = run_with_test_server(
707
+ self.get_default_request(), self.URL, self.default_handler
708
+ )
709
+ url = f"http://{host}:{port}/test-path"
710
+ attributes = {
711
+ HTTP_REQUEST_METHOD: "GET",
712
+ SpanAttributes.HTTP_METHOD: "GET",
713
+ URL_FULL: url,
714
+ SpanAttributes.HTTP_URL: url,
715
+ HTTP_RESPONSE_STATUS_CODE: 200,
716
+ SpanAttributes.HTTP_STATUS_CODE: 200,
717
+ }
718
+ span = self.assert_spans(1)
719
+ self.assertEqual("GET", span.name)
720
+ self.assertEqual(span.attributes, attributes)
721
+
422
722
  def test_instrument_with_custom_trace_config(self):
423
723
  trace_config = aiohttp.TraceConfig()
424
724