aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/__init__.py')
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/__init__.py477
1 files changed, 477 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/__init__.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/__init__.py
new file mode 100644
index 00000000..a80e6d07
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/__init__.py
@@ -0,0 +1,477 @@
+# Copyright The OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+This library allows tracing HTTP requests made by the
+`urllib <https://docs.python.org/3/library/urllib>`_ library.
+
+Usage
+-----
+.. code-block:: python
+
+ from urllib import request
+ from opentelemetry.instrumentation.urllib import URLLibInstrumentor
+
+ # You can optionally pass a custom TracerProvider to
+ # URLLibInstrumentor().instrument()
+
+ URLLibInstrumentor().instrument()
+ req = request.Request('https://postman-echo.com/post', method="POST")
+ r = request.urlopen(req)
+
+Configuration
+-------------
+
+Request/Response hooks
+**********************
+
+The urllib instrumentation supports extending tracing behavior with the help of
+request and response hooks. These are functions that are called back by the instrumentation
+right after a Span is created for a request and right before the span is finished processing a response respectively.
+The hooks can be configured as follows:
+
+.. code:: python
+
+ from http.client import HTTPResponse
+ from urllib.request import Request
+
+ from opentelemetry.instrumentation.urllib import URLLibInstrumentor
+ from opentelemetry.trace import Span
+
+
+ def request_hook(span: Span, request: Request):
+ pass
+
+
+ def response_hook(span: Span, request: Request, response: HTTPResponse):
+ pass
+
+
+ URLLibInstrumentor().instrument(
+ request_hook=request_hook,
+ response_hook=response_hook
+ )
+
+Exclude lists
+*************
+
+To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_URLLIB_EXCLUDED_URLS``
+(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude.
+
+For example,
+
+::
+
+ export OTEL_PYTHON_URLLIB_EXCLUDED_URLS="client/.*/info,healthcheck"
+
+will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
+
+API
+---
+"""
+
+from __future__ import annotations
+
+import functools
+import types
+import typing
+from http import client
+from timeit import default_timer
+from typing import Any, Collection
+from urllib.request import ( # pylint: disable=no-name-in-module,import-error
+ OpenerDirector,
+ Request,
+)
+
+from opentelemetry.instrumentation._semconv import (
+ _client_duration_attrs_new,
+ _client_duration_attrs_old,
+ _filter_semconv_duration_attrs,
+ _get_schema_url,
+ _OpenTelemetrySemanticConventionStability,
+ _OpenTelemetryStabilitySignalType,
+ _report_new,
+ _report_old,
+ _set_http_method,
+ _set_http_network_protocol_version,
+ _set_http_url,
+ _set_status,
+ _StabilityMode,
+)
+from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.instrumentation.urllib.package import _instruments
+from opentelemetry.instrumentation.urllib.version import __version__
+from opentelemetry.instrumentation.utils import (
+ is_http_instrumentation_enabled,
+ suppress_http_instrumentation,
+)
+from opentelemetry.metrics import Histogram, Meter, get_meter
+from opentelemetry.propagate import inject
+from opentelemetry.semconv._incubating.metrics.http_metrics import (
+ HTTP_CLIENT_REQUEST_BODY_SIZE,
+ HTTP_CLIENT_RESPONSE_BODY_SIZE,
+ create_http_client_request_body_size,
+ create_http_client_response_body_size,
+)
+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.semconv.trace import SpanAttributes
+from opentelemetry.trace import Span, SpanKind, Tracer, get_tracer
+from opentelemetry.util.http import (
+ ExcludeList,
+ get_excluded_urls,
+ parse_excluded_urls,
+ remove_url_credentials,
+ sanitize_method,
+)
+from opentelemetry.util.types import Attributes
+
+_excluded_urls_from_env = get_excluded_urls("URLLIB")
+
+_RequestHookT = typing.Optional[typing.Callable[[Span, Request], None]]
+_ResponseHookT = typing.Optional[
+ typing.Callable[[Span, Request, client.HTTPResponse], None]
+]
+
+
+class URLLibInstrumentor(BaseInstrumentor):
+ """An instrumentor for urllib
+ See `BaseInstrumentor`
+ """
+
+ def instrumentation_dependencies(self) -> Collection[str]:
+ return _instruments
+
+ def _instrument(self, **kwargs: Any):
+ """Instruments urllib module
+
+ Args:
+ **kwargs: Optional arguments
+ ``tracer_provider``: a TracerProvider, defaults to global
+ ``request_hook``: An optional callback invoked that is invoked right after a span is created.
+ ``response_hook``: An optional callback which is invoked right before the span is finished processing a response
+ ``excluded_urls``: A string containing a comma-delimited
+ list of regexes used to exclude URLs from tracking
+ """
+ # initialize semantic conventions opt-in if needed
+ _OpenTelemetrySemanticConventionStability._initialize()
+ sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
+ _OpenTelemetryStabilitySignalType.HTTP,
+ )
+ schema_url = _get_schema_url(sem_conv_opt_in_mode)
+ tracer_provider = kwargs.get("tracer_provider")
+ tracer = get_tracer(
+ __name__,
+ __version__,
+ tracer_provider,
+ schema_url=schema_url,
+ )
+ excluded_urls = kwargs.get("excluded_urls")
+ meter_provider = kwargs.get("meter_provider")
+ meter = get_meter(
+ __name__,
+ __version__,
+ meter_provider,
+ schema_url=schema_url,
+ )
+
+ histograms = _create_client_histograms(meter, sem_conv_opt_in_mode)
+
+ _instrument(
+ tracer,
+ histograms,
+ request_hook=kwargs.get("request_hook"),
+ response_hook=kwargs.get("response_hook"),
+ excluded_urls=(
+ _excluded_urls_from_env
+ if excluded_urls is None
+ else parse_excluded_urls(excluded_urls)
+ ),
+ sem_conv_opt_in_mode=sem_conv_opt_in_mode,
+ )
+
+ def _uninstrument(self, **kwargs: Any):
+ _uninstrument()
+
+ def uninstrument_opener(self, opener: OpenerDirector): # pylint: disable=no-self-use
+ """uninstrument_opener a specific instance of urllib.request.OpenerDirector"""
+ _uninstrument_from(opener, restore_as_bound_func=True)
+
+
+# pylint: disable=too-many-statements
+def _instrument(
+ tracer: Tracer,
+ histograms: dict[str, Histogram],
+ request_hook: _RequestHookT = None,
+ response_hook: _ResponseHookT = None,
+ excluded_urls: ExcludeList | None = None,
+ sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+):
+ """Enables tracing of all requests calls that go through
+ :code:`urllib.Client._make_request`"""
+
+ opener_open = OpenerDirector.open
+
+ @functools.wraps(opener_open)
+ def instrumented_open(opener, fullurl, data=None, timeout=None):
+ if isinstance(fullurl, str):
+ request_ = Request(fullurl, data)
+ else:
+ request_ = fullurl
+
+ def get_or_create_headers():
+ return getattr(request_, "headers", {})
+
+ def call_wrapped():
+ return opener_open(opener, request_, data=data, timeout=timeout)
+
+ return _instrumented_open_call(
+ opener, request_, call_wrapped, get_or_create_headers
+ )
+
+ def _instrumented_open_call(
+ _, request, call_wrapped, get_or_create_headers
+ ): # pylint: disable=too-many-locals
+ if not is_http_instrumentation_enabled():
+ return call_wrapped()
+
+ url = request.full_url
+ if excluded_urls and excluded_urls.url_disabled(url):
+ return call_wrapped()
+
+ method = request.get_method().upper()
+
+ span_name = _get_span_name(method)
+
+ url = remove_url_credentials(url)
+
+ data = getattr(request, "data", None)
+ request_size = 0 if data is None else len(data)
+
+ labels = {}
+
+ _set_http_method(
+ labels,
+ method,
+ sanitize_method(method),
+ sem_conv_opt_in_mode,
+ )
+ _set_http_url(labels, url, sem_conv_opt_in_mode)
+
+ with tracer.start_as_current_span(
+ span_name, kind=SpanKind.CLIENT, attributes=labels
+ ) as span:
+ exception = None
+ if callable(request_hook):
+ request_hook(span, request)
+
+ headers = get_or_create_headers()
+ inject(headers)
+
+ with suppress_http_instrumentation():
+ start_time = default_timer()
+ try:
+ result = call_wrapped() # *** PROCEED
+ except Exception as exc: # pylint: disable=W0703
+ exception = exc
+ result = getattr(exc, "file", None)
+ finally:
+ duration_s = default_timer() - start_time
+ response_size = 0
+ if result is not None:
+ response_size = int(result.headers.get("Content-Length", 0))
+ code_ = result.getcode()
+ # set http status code based on semconv
+ if code_:
+ _set_status_code_attribute(
+ span, code_, labels, sem_conv_opt_in_mode
+ )
+
+ ver_ = str(getattr(result, "version", ""))
+ if ver_:
+ _set_http_network_protocol_version(
+ labels, f"{ver_[:1]}.{ver_[:-1]}", sem_conv_opt_in_mode
+ )
+
+ if exception is not None and _report_new(sem_conv_opt_in_mode):
+ span.set_attribute(ERROR_TYPE, type(exception).__qualname__)
+ labels[ERROR_TYPE] = type(exception).__qualname__
+
+ duration_attrs_old = _filter_semconv_duration_attrs(
+ labels,
+ _client_duration_attrs_old,
+ _client_duration_attrs_new,
+ sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
+ )
+ duration_attrs_new = _filter_semconv_duration_attrs(
+ labels,
+ _client_duration_attrs_old,
+ _client_duration_attrs_new,
+ sem_conv_opt_in_mode=_StabilityMode.HTTP,
+ )
+
+ duration_attrs_old[SpanAttributes.HTTP_URL] = url
+
+ _record_histograms(
+ histograms,
+ duration_attrs_old,
+ duration_attrs_new,
+ request_size,
+ response_size,
+ duration_s,
+ sem_conv_opt_in_mode,
+ )
+
+ if callable(response_hook):
+ response_hook(span, request, result)
+
+ if exception is not None:
+ raise exception.with_traceback(exception.__traceback__)
+
+ return result
+
+ instrumented_open.opentelemetry_instrumentation_urllib_applied = True
+ OpenerDirector.open = instrumented_open
+
+
+def _uninstrument():
+ """Disables instrumentation of :code:`urllib` through this module.
+
+ Note that this only works if no other module also patches urllib."""
+ _uninstrument_from(OpenerDirector)
+
+
+def _uninstrument_from(instr_root, restore_as_bound_func: bool = False):
+ instr_func_name = "open"
+ instr_func = getattr(instr_root, instr_func_name)
+ if not getattr(
+ instr_func,
+ "opentelemetry_instrumentation_urllib_applied",
+ False,
+ ):
+ return
+
+ original = instr_func.__wrapped__ # pylint:disable=no-member
+ if restore_as_bound_func:
+ original = types.MethodType(original, instr_root)
+ setattr(instr_root, instr_func_name, original)
+
+
+def _get_span_name(method: str) -> str:
+ method = sanitize_method(method.strip())
+ if method == "_OTHER":
+ method = "HTTP"
+ return method
+
+
+def _set_status_code_attribute(
+ span: Span,
+ status_code: int,
+ metric_attributes: dict[str, Any] | None = None,
+ sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+) -> None:
+ status_code_str = str(status_code)
+ try:
+ status_code = int(status_code)
+ except ValueError:
+ status_code = -1
+
+ if metric_attributes is None:
+ metric_attributes = {}
+
+ _set_status(
+ span,
+ metric_attributes,
+ status_code,
+ status_code_str,
+ server_span=False,
+ sem_conv_opt_in_mode=sem_conv_opt_in_mode,
+ )
+
+
+def _create_client_histograms(
+ meter: Meter, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT
+) -> dict[str, Histogram]:
+ histograms = {}
+ if _report_old(sem_conv_opt_in_mode):
+ histograms[MetricInstruments.HTTP_CLIENT_DURATION] = (
+ meter.create_histogram(
+ name=MetricInstruments.HTTP_CLIENT_DURATION,
+ unit="ms",
+ description="Measures the duration of the outbound HTTP request",
+ )
+ )
+ histograms[MetricInstruments.HTTP_CLIENT_REQUEST_SIZE] = (
+ meter.create_histogram(
+ name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE,
+ unit="By",
+ description="Measures the size of HTTP request messages.",
+ )
+ )
+ histograms[MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE] = (
+ meter.create_histogram(
+ name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE,
+ unit="By",
+ description="Measures the size of HTTP response messages.",
+ )
+ )
+ if _report_new(sem_conv_opt_in_mode):
+ histograms[HTTP_CLIENT_REQUEST_DURATION] = meter.create_histogram(
+ name=HTTP_CLIENT_REQUEST_DURATION,
+ unit="s",
+ description="Duration of HTTP client requests.",
+ )
+ histograms[HTTP_CLIENT_REQUEST_BODY_SIZE] = (
+ create_http_client_request_body_size(meter)
+ )
+ histograms[HTTP_CLIENT_RESPONSE_BODY_SIZE] = (
+ create_http_client_response_body_size(meter)
+ )
+
+ return histograms
+
+
+def _record_histograms(
+ histograms: dict[str, Histogram],
+ metric_attributes_old: Attributes,
+ metric_attributes_new: Attributes,
+ request_size: int,
+ response_size: int,
+ duration_s: float,
+ sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+):
+ if _report_old(sem_conv_opt_in_mode):
+ duration = max(round(duration_s * 1000), 0)
+ histograms[MetricInstruments.HTTP_CLIENT_DURATION].record(
+ duration, attributes=metric_attributes_old
+ )
+ histograms[MetricInstruments.HTTP_CLIENT_REQUEST_SIZE].record(
+ request_size, attributes=metric_attributes_old
+ )
+ histograms[MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE].record(
+ response_size, attributes=metric_attributes_old
+ )
+ if _report_new(sem_conv_opt_in_mode):
+ histograms[HTTP_CLIENT_REQUEST_DURATION].record(
+ duration_s, attributes=metric_attributes_new
+ )
+ histograms[HTTP_CLIENT_REQUEST_BODY_SIZE].record(
+ request_size, attributes=metric_attributes_new
+ )
+ histograms[HTTP_CLIENT_RESPONSE_BODY_SIZE].record(
+ response_size, attributes=metric_attributes_new
+ )