Skip to content

Commit c87ffd4

Browse files
authored
HTTP semantic convention stability migration for urllib (open-telemetry#2736)
1 parent d563f8d commit c87ffd4

File tree

6 files changed

+572
-64
lines changed

6 files changed

+572
-64
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5656
([#2715](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2715))
5757
- `opentelemetry-instrumentation-django` Implement new semantic convention opt-in with stable http semantic conventions
5858
([#2714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2714))
59+
- `opentelemetry-instrumentation-urllib` Implement new semantic convention opt-in migration
60+
([#2736](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2736))
5961

6062
### Breaking changes
6163

@@ -71,6 +73,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7173
([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682))
7274
- Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `django` middleware
7375
([#2714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2714))
76+
- Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `urllib` instrumentation
77+
([#2736](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2736))
7478
- `opentelemetry-instrumentation-httpx`, `opentelemetry-instrumentation-aiohttp-client`,
7579
`opentelemetry-instrumentation-requests` Populate `{method}` as `HTTP` on `_OTHER` methods
7680
([#2726](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2726))

instrumentation/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,6 @@
4545
| [opentelemetry-instrumentation-threading](./opentelemetry-instrumentation-threading) | threading | No | experimental
4646
| [opentelemetry-instrumentation-tornado](./opentelemetry-instrumentation-tornado) | tornado >= 5.1.1 | Yes | experimental
4747
| [opentelemetry-instrumentation-tortoiseorm](./opentelemetry-instrumentation-tortoiseorm) | tortoise-orm >= 0.17.0 | No | experimental
48-
| [opentelemetry-instrumentation-urllib](./opentelemetry-instrumentation-urllib) | urllib | Yes | experimental
48+
| [opentelemetry-instrumentation-urllib](./opentelemetry-instrumentation-urllib) | urllib | Yes | migration
4949
| [opentelemetry-instrumentation-urllib3](./opentelemetry-instrumentation-urllib3) | urllib3 >= 1.0.0, < 3.0.0 | Yes | migration
5050
| [opentelemetry-instrumentation-wsgi](./opentelemetry-instrumentation-wsgi) | wsgi | Yes | migration

instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/__init__.py

Lines changed: 184 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -85,25 +85,49 @@ def response_hook(span, request_obj, response)
8585
Request,
8686
)
8787

88+
from opentelemetry.instrumentation._semconv import (
89+
_client_duration_attrs_new,
90+
_client_duration_attrs_old,
91+
_filter_semconv_duration_attrs,
92+
_get_schema_url,
93+
_HTTPStabilityMode,
94+
_OpenTelemetrySemanticConventionStability,
95+
_OpenTelemetryStabilitySignalType,
96+
_report_new,
97+
_report_old,
98+
_set_http_method,
99+
_set_http_network_protocol_version,
100+
_set_http_url,
101+
_set_status,
102+
)
88103
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
89104
from opentelemetry.instrumentation.urllib.package import _instruments
90105
from opentelemetry.instrumentation.urllib.version import __version__
91106
from opentelemetry.instrumentation.utils import (
92-
http_status_to_status_code,
93107
is_http_instrumentation_enabled,
94108
suppress_http_instrumentation,
95109
)
96110
from opentelemetry.metrics import Histogram, get_meter
97111
from opentelemetry.propagate import inject
112+
from opentelemetry.semconv._incubating.metrics.http_metrics import (
113+
HTTP_CLIENT_REQUEST_BODY_SIZE,
114+
HTTP_CLIENT_RESPONSE_BODY_SIZE,
115+
create_http_client_request_body_size,
116+
create_http_client_response_body_size,
117+
)
118+
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
98119
from opentelemetry.semconv.metrics import MetricInstruments
120+
from opentelemetry.semconv.metrics.http_metrics import (
121+
HTTP_CLIENT_REQUEST_DURATION,
122+
)
99123
from opentelemetry.semconv.trace import SpanAttributes
100124
from opentelemetry.trace import Span, SpanKind, get_tracer
101-
from opentelemetry.trace.status import Status
102125
from opentelemetry.util.http import (
103126
ExcludeList,
104127
get_excluded_urls,
105128
parse_excluded_urls,
106129
remove_url_credentials,
130+
sanitize_method,
107131
)
108132

109133
_excluded_urls_from_env = get_excluded_urls("URLLIB")
@@ -133,23 +157,29 @@ def _instrument(self, **kwargs):
133157
``excluded_urls``: A string containing a comma-delimited
134158
list of regexes used to exclude URLs from tracking
135159
"""
160+
# initialize semantic conventions opt-in if needed
161+
_OpenTelemetrySemanticConventionStability._initialize()
162+
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
163+
_OpenTelemetryStabilitySignalType.HTTP,
164+
)
165+
schema_url = _get_schema_url(sem_conv_opt_in_mode)
136166
tracer_provider = kwargs.get("tracer_provider")
137167
tracer = get_tracer(
138168
__name__,
139169
__version__,
140170
tracer_provider,
141-
schema_url="https://opentelemetry.io/schemas/1.11.0",
171+
schema_url=schema_url,
142172
)
143173
excluded_urls = kwargs.get("excluded_urls")
144174
meter_provider = kwargs.get("meter_provider")
145175
meter = get_meter(
146176
__name__,
147177
__version__,
148178
meter_provider,
149-
schema_url="https://opentelemetry.io/schemas/1.11.0",
179+
schema_url=schema_url,
150180
)
151181

152-
histograms = _create_client_histograms(meter)
182+
histograms = _create_client_histograms(meter, sem_conv_opt_in_mode)
153183

154184
_instrument(
155185
tracer,
@@ -161,6 +191,7 @@ def _instrument(self, **kwargs):
161191
if excluded_urls is None
162192
else parse_excluded_urls(excluded_urls)
163193
),
194+
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
164195
)
165196

166197
def _uninstrument(self, **kwargs):
@@ -173,12 +204,14 @@ def uninstrument_opener(
173204
_uninstrument_from(opener, restore_as_bound_func=True)
174205

175206

207+
# pylint: disable=too-many-statements
176208
def _instrument(
177209
tracer,
178210
histograms: Dict[str, Histogram],
179211
request_hook: _RequestHookT = None,
180212
response_hook: _ResponseHookT = None,
181213
excluded_urls: ExcludeList = None,
214+
sem_conv_opt_in_mode: _HTTPStabilityMode = _HTTPStabilityMode.DEFAULT,
182215
):
183216
"""Enables tracing of all requests calls that go through
184217
:code:`urllib.Client._make_request`"""
@@ -214,14 +247,22 @@ def _instrumented_open_call(
214247

215248
method = request.get_method().upper()
216249

217-
span_name = method.strip()
250+
span_name = _get_span_name(method)
218251

219252
url = remove_url_credentials(url)
220253

221-
labels = {
222-
SpanAttributes.HTTP_METHOD: method,
223-
SpanAttributes.HTTP_URL: url,
224-
}
254+
data = getattr(request, "data", None)
255+
request_size = 0 if data is None else len(data)
256+
257+
labels = {}
258+
259+
_set_http_method(
260+
labels,
261+
method,
262+
sanitize_method(method),
263+
sem_conv_opt_in_mode,
264+
)
265+
_set_http_url(labels, url, sem_conv_opt_in_mode)
225266

226267
with tracer.start_as_current_span(
227268
span_name, kind=SpanKind.CLIENT, attributes=labels
@@ -241,24 +282,50 @@ def _instrumented_open_call(
241282
exception = exc
242283
result = getattr(exc, "file", None)
243284
finally:
244-
elapsed_time = round((default_timer() - start_time) * 1000)
245-
285+
duration_s = default_timer() - start_time
286+
response_size = 0
246287
if result is not None:
288+
response_size = int(result.headers.get("Content-Length", 0))
247289
code_ = result.getcode()
248-
labels[SpanAttributes.HTTP_STATUS_CODE] = str(code_)
249-
250-
if span.is_recording() and code_ is not None:
251-
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, code_)
252-
span.set_status(Status(http_status_to_status_code(code_)))
290+
# set http status code based on semconv
291+
if code_:
292+
_set_status_code_attribute(
293+
span, code_, labels, sem_conv_opt_in_mode
294+
)
253295

254296
ver_ = str(getattr(result, "version", ""))
255297
if ver_:
256-
labels[SpanAttributes.HTTP_FLAVOR] = (
257-
f"{ver_[:1]}.{ver_[:-1]}"
298+
_set_http_network_protocol_version(
299+
labels, f"{ver_[:1]}.{ver_[:-1]}", sem_conv_opt_in_mode
258300
)
259301

302+
if exception is not None and _report_new(sem_conv_opt_in_mode):
303+
span.set_attribute(ERROR_TYPE, type(exception).__qualname__)
304+
labels[ERROR_TYPE] = type(exception).__qualname__
305+
306+
duration_attrs_old = _filter_semconv_duration_attrs(
307+
labels,
308+
_client_duration_attrs_old,
309+
_client_duration_attrs_new,
310+
sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT,
311+
)
312+
duration_attrs_new = _filter_semconv_duration_attrs(
313+
labels,
314+
_client_duration_attrs_old,
315+
_client_duration_attrs_new,
316+
sem_conv_opt_in_mode=_HTTPStabilityMode.HTTP,
317+
)
318+
319+
duration_attrs_old[SpanAttributes.HTTP_URL] = url
320+
260321
_record_histograms(
261-
histograms, labels, request, result, elapsed_time
322+
histograms,
323+
duration_attrs_old,
324+
duration_attrs_new,
325+
request_size,
326+
response_size,
327+
duration_s,
328+
sem_conv_opt_in_mode,
262329
)
263330

264331
if callable(response_hook):
@@ -296,43 +363,108 @@ def _uninstrument_from(instr_root, restore_as_bound_func=False):
296363
setattr(instr_root, instr_func_name, original)
297364

298365

299-
def _create_client_histograms(meter) -> Dict[str, Histogram]:
300-
histograms = {
301-
MetricInstruments.HTTP_CLIENT_DURATION: meter.create_histogram(
302-
name=MetricInstruments.HTTP_CLIENT_DURATION,
303-
unit="ms",
304-
description="Measures the duration of outbound HTTP requests.",
305-
),
306-
MetricInstruments.HTTP_CLIENT_REQUEST_SIZE: meter.create_histogram(
307-
name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE,
308-
unit="By",
309-
description="Measures the size of HTTP request messages.",
310-
),
311-
MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE: meter.create_histogram(
312-
name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE,
313-
unit="By",
314-
description="Measures the size of HTTP response messages.",
315-
),
316-
}
366+
def _get_span_name(method: str) -> str:
367+
method = sanitize_method(method.strip())
368+
if method == "_OTHER":
369+
method = "HTTP"
370+
return method
371+
372+
373+
def _set_status_code_attribute(
374+
span: Span,
375+
status_code: int,
376+
metric_attributes: dict = None,
377+
sem_conv_opt_in_mode: _HTTPStabilityMode = _HTTPStabilityMode.DEFAULT,
378+
) -> None:
379+
380+
status_code_str = str(status_code)
381+
try:
382+
status_code = int(status_code)
383+
except ValueError:
384+
status_code = -1
385+
386+
if metric_attributes is None:
387+
metric_attributes = {}
388+
389+
_set_status(
390+
span,
391+
metric_attributes,
392+
status_code,
393+
status_code_str,
394+
server_span=False,
395+
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
396+
)
397+
398+
399+
def _create_client_histograms(
400+
meter, sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT
401+
) -> Dict[str, Histogram]:
402+
histograms = {}
403+
if _report_old(sem_conv_opt_in_mode):
404+
histograms[MetricInstruments.HTTP_CLIENT_DURATION] = (
405+
meter.create_histogram(
406+
name=MetricInstruments.HTTP_CLIENT_DURATION,
407+
unit="ms",
408+
description="Measures the duration of the outbound HTTP request",
409+
)
410+
)
411+
histograms[MetricInstruments.HTTP_CLIENT_REQUEST_SIZE] = (
412+
meter.create_histogram(
413+
name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE,
414+
unit="By",
415+
description="Measures the size of HTTP request messages.",
416+
)
417+
)
418+
histograms[MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE] = (
419+
meter.create_histogram(
420+
name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE,
421+
unit="By",
422+
description="Measures the size of HTTP response messages.",
423+
)
424+
)
425+
if _report_new(sem_conv_opt_in_mode):
426+
histograms[HTTP_CLIENT_REQUEST_DURATION] = meter.create_histogram(
427+
name=HTTP_CLIENT_REQUEST_DURATION,
428+
unit="s",
429+
description="Duration of HTTP client requests.",
430+
)
431+
histograms[HTTP_CLIENT_REQUEST_BODY_SIZE] = (
432+
create_http_client_request_body_size(meter)
433+
)
434+
histograms[HTTP_CLIENT_RESPONSE_BODY_SIZE] = (
435+
create_http_client_response_body_size(meter)
436+
)
317437

318438
return histograms
319439

320440

321441
def _record_histograms(
322-
histograms, metric_attributes, request, response, elapsed_time
442+
histograms: Dict[str, Histogram],
443+
metric_attributes_old: dict,
444+
metric_attributes_new: dict,
445+
request_size: int,
446+
response_size: int,
447+
duration_s: float,
448+
sem_conv_opt_in_mode: _HTTPStabilityMode = _HTTPStabilityMode.DEFAULT,
323449
):
324-
histograms[MetricInstruments.HTTP_CLIENT_DURATION].record(
325-
elapsed_time, attributes=metric_attributes
326-
)
327-
328-
data = getattr(request, "data", None)
329-
request_size = 0 if data is None else len(data)
330-
histograms[MetricInstruments.HTTP_CLIENT_REQUEST_SIZE].record(
331-
request_size, attributes=metric_attributes
332-
)
333-
334-
if response is not None:
335-
response_size = int(response.headers.get("Content-Length", 0))
450+
if _report_old(sem_conv_opt_in_mode):
451+
duration = max(round(duration_s * 1000), 0)
452+
histograms[MetricInstruments.HTTP_CLIENT_DURATION].record(
453+
duration, attributes=metric_attributes_old
454+
)
455+
histograms[MetricInstruments.HTTP_CLIENT_REQUEST_SIZE].record(
456+
request_size, attributes=metric_attributes_old
457+
)
336458
histograms[MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE].record(
337-
response_size, attributes=metric_attributes
459+
response_size, attributes=metric_attributes_old
460+
)
461+
if _report_new(sem_conv_opt_in_mode):
462+
histograms[HTTP_CLIENT_REQUEST_DURATION].record(
463+
duration_s, attributes=metric_attributes_new
464+
)
465+
histograms[HTTP_CLIENT_REQUEST_BODY_SIZE].record(
466+
request_size, attributes=metric_attributes_new
467+
)
468+
histograms[HTTP_CLIENT_RESPONSE_BODY_SIZE].record(
469+
response_size, attributes=metric_attributes_new
338470
)

instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/package.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@
1616
_instruments = tuple()
1717

1818
_supports_metrics = True
19+
20+
_semconv_status = "migration"

0 commit comments

Comments
 (0)