diff options
author | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
---|---|---|
committer | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
commit | 4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch) | |
tree | ee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/opentelemetry/util/http | |
parent | cc961e04ba734dd72309fb548a2f97d67d578813 (diff) | |
download | gn-ai-master.tar.gz |
Diffstat (limited to '.venv/lib/python3.12/site-packages/opentelemetry/util/http')
4 files changed, 467 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/util/http/__init__.py b/.venv/lib/python3.12/site-packages/opentelemetry/util/http/__init__.py new file mode 100644 index 00000000..71a6403a --- /dev/null +++ b/.venv/lib/python3.12/site-packages/opentelemetry/util/http/__init__.py @@ -0,0 +1,257 @@ +# 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. + +from __future__ import annotations + +from collections.abc import Mapping +from os import environ +from re import IGNORECASE as RE_IGNORECASE +from re import compile as re_compile +from re import search +from typing import Callable, Iterable, overload +from urllib.parse import urlparse, urlunparse + +from opentelemetry.semconv.trace import SpanAttributes + +OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS = ( + "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS" +) +OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST = ( + "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST" +) +OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE = ( + "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE" +) + +OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS = ( + "OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS" +) + +# List of recommended metrics attributes +_duration_attrs = { + SpanAttributes.HTTP_METHOD, + SpanAttributes.HTTP_HOST, + SpanAttributes.HTTP_SCHEME, + SpanAttributes.HTTP_STATUS_CODE, + SpanAttributes.HTTP_FLAVOR, + SpanAttributes.HTTP_SERVER_NAME, + SpanAttributes.NET_HOST_NAME, + SpanAttributes.NET_HOST_PORT, +} + +_active_requests_count_attrs = { + SpanAttributes.HTTP_METHOD, + SpanAttributes.HTTP_HOST, + SpanAttributes.HTTP_SCHEME, + SpanAttributes.HTTP_FLAVOR, + SpanAttributes.HTTP_SERVER_NAME, +} + + +class ExcludeList: + """Class to exclude certain paths (given as a list of regexes) from tracing requests""" + + def __init__(self, excluded_urls: Iterable[str]): + self._excluded_urls = excluded_urls + if self._excluded_urls: + self._regex = re_compile("|".join(excluded_urls)) + + def url_disabled(self, url: str) -> bool: + return bool(self._excluded_urls and search(self._regex, url)) + + +class SanitizeValue: + """Class to sanitize (remove sensitive data from) certain headers (given as a list of regexes)""" + + def __init__(self, sanitized_fields: Iterable[str]): + self._sanitized_fields = sanitized_fields + if self._sanitized_fields: + self._regex = re_compile("|".join(sanitized_fields), RE_IGNORECASE) + + def sanitize_header_value(self, header: str, value: str) -> str: + return ( + "[REDACTED]" + if (self._sanitized_fields and search(self._regex, header)) + else value + ) + + def sanitize_header_values( + self, + headers: Mapping[str, str | list[str]], + header_regexes: list[str], + normalize_function: Callable[[str], str], + ) -> dict[str, list[str]]: + values: dict[str, list[str]] = {} + + if header_regexes: + header_regexes_compiled = re_compile( + "|".join(header_regexes), + RE_IGNORECASE, + ) + + for header_name, header_value in headers.items(): + if header_regexes_compiled.fullmatch(header_name): + key = normalize_function(header_name.lower()) + if isinstance(header_value, str): + values[key] = [ + self.sanitize_header_value( + header_name, header_value + ) + ] + else: + values[key] = [ + self.sanitize_header_value(header_name, value) + for value in header_value + ] + + return values + + +_root = r"OTEL_PYTHON_{}" + + +def get_traced_request_attrs(instrumentation: str) -> list[str]: + traced_request_attrs = environ.get( + _root.format(f"{instrumentation}_TRACED_REQUEST_ATTRS") + ) + if traced_request_attrs: + return [ + traced_request_attr.strip() + for traced_request_attr in traced_request_attrs.split(",") + ] + return [] + + +def get_excluded_urls(instrumentation: str) -> ExcludeList: + # Get instrumentation-specific excluded URLs. If not set, retrieve them + # from generic variable. + excluded_urls = environ.get( + _root.format(f"{instrumentation}_EXCLUDED_URLS"), + environ.get(_root.format("EXCLUDED_URLS"), ""), + ) + + return parse_excluded_urls(excluded_urls) + + +def parse_excluded_urls(excluded_urls: str) -> ExcludeList: + """ + Small helper to put an arbitrary url list inside an ExcludeList + """ + if excluded_urls: + excluded_url_list = [ + excluded_url.strip() for excluded_url in excluded_urls.split(",") + ] + else: + excluded_url_list = [] + + return ExcludeList(excluded_url_list) + + +def remove_url_credentials(url: str) -> str: + """Given a string url, remove the username and password only if it is a valid url""" + + try: + parsed = urlparse(url) + if all([parsed.scheme, parsed.netloc]): # checks for valid url + parsed_url = urlparse(url) + _, _, netloc = parsed.netloc.rpartition("@") + return urlunparse( + ( + parsed_url.scheme, + netloc, + parsed_url.path, + parsed_url.params, + parsed_url.query, + parsed_url.fragment, + ) + ) + except ValueError: # an unparsable url was passed + pass + return url + + +def normalise_request_header_name(header: str) -> str: + key = header.lower().replace("-", "_") + return f"http.request.header.{key}" + + +def normalise_response_header_name(header: str) -> str: + key = header.lower().replace("-", "_") + return f"http.response.header.{key}" + + +@overload +def sanitize_method(method: str) -> str: ... + + +@overload +def sanitize_method(method: None) -> None: ... + + +def sanitize_method(method: str | None) -> str | None: + if method is None: + return None + method = method.upper() + if ( + environ.get(OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS) + or + # Based on https://www.rfc-editor.org/rfc/rfc7231#section-4.1 and https://www.rfc-editor.org/rfc/rfc5789#section-2. + method + in [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "CONNECT", + "OPTIONS", + "TRACE", + "PATCH", + ] + ): + return method + return "_OTHER" + + +def get_custom_headers(env_var: str) -> list[str]: + custom_headers = environ.get(env_var, None) + if custom_headers: + return [ + custom_headers.strip() + for custom_headers in custom_headers.split(",") + ] + return [] + + +def _parse_active_request_count_attrs(req_attrs): + active_requests_count_attrs = { + key: req_attrs[key] + for key in _active_requests_count_attrs.intersection(req_attrs.keys()) + } + return active_requests_count_attrs + + +def _parse_duration_attrs(req_attrs): + duration_attrs = { + key: req_attrs[key] + for key in _duration_attrs.intersection(req_attrs.keys()) + } + return duration_attrs + + +def _parse_url_query(url: str): + parsed_url = urlparse(url) + path = parsed_url.path + query_params = parsed_url.query + return path, query_params diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/util/http/httplib.py b/.venv/lib/python3.12/site-packages/opentelemetry/util/http/httplib.py new file mode 100644 index 00000000..f375e2f7 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/opentelemetry/util/http/httplib.py @@ -0,0 +1,195 @@ +# 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 provides functionality to enrich HTTP client spans with IPs. It does +not create spans on its own. +""" + +from __future__ import annotations + +import contextlib +import http.client +import logging +import socket # pylint:disable=unused-import # Used for typing +import typing +from typing import Any, Callable, Collection, TypedDict, cast + +import wrapt + +from opentelemetry import context +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import unwrap +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace.span import Span + +_STATE_KEY = "httpbase_instrumentation_state" + +logger = logging.getLogger(__name__) + +R = typing.TypeVar("R") + + +class HttpClientInstrumentor(BaseInstrumentor): + def instrumentation_dependencies(self) -> Collection[str]: + return () # This instruments http.client from stdlib; no extra deps. + + def _instrument(self, **kwargs: Any): + """Instruments the http.client module (not creating spans on its own)""" + _instrument() + + def _uninstrument(self, **kwargs: Any): + _uninstrument() + + +def _remove_nonrecording(spanlist: list[Span]) -> bool: + idx = len(spanlist) - 1 + while idx >= 0: + if not spanlist[idx].is_recording(): + logger.debug("Span is not recording: %s", spanlist[idx]) + islast = idx + 1 == len(spanlist) + if not islast: + spanlist[idx] = spanlist[len(spanlist) - 1] + spanlist.pop() + if islast: + if idx == 0: + return False # We removed everything + idx -= 1 + else: + idx -= 1 + return True + + +def trysetip( + conn: http.client.HTTPConnection, loglevel: int = logging.DEBUG +) -> bool: + """Tries to set the net.peer.ip semantic attribute on the current span from the given + HttpConnection. + + Returns False if the connection is not yet established, False if the IP was captured + or there is no need to capture it. + """ + + state = _getstate() + if not state: + return True + spanlist: typing.List[Span] = state.get("need_ip") + if not spanlist: + return True + + # Remove all non-recording spans from the list. + if not _remove_nonrecording(spanlist): + return True + + sock = "<property not accessed>" + try: + sock: typing.Optional[socket.socket] = conn.sock + logger.debug("Got socket: %s", sock) + if sock is None: + return False + addr = sock.getpeername() + if addr and addr[0]: + ip = addr[0] + except Exception: # pylint:disable=broad-except + logger.log( + loglevel, + "Failed to get peer address from %s", + sock, + exc_info=True, + stack_info=True, + ) + else: + for span in spanlist: + span.set_attribute(SpanAttributes.NET_PEER_IP, ip) + return True + + +def _instrumented_connect( + wrapped: Callable[..., R], + instance: http.client.HTTPConnection, + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> R: + result = wrapped(*args, **kwargs) + trysetip(instance, loglevel=logging.WARNING) + return result + + +def instrument_connect(module: type[Any], name: str = "connect"): + """Instrument additional connect() methods, e.g. for derived classes.""" + + wrapt.wrap_function_wrapper( + module, + name, + _instrumented_connect, + ) + + +def _instrument(): + def instrumented_send( + wrapped: Callable[..., R], + instance: http.client.HTTPConnection, + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> R: + done = trysetip(instance) + result = wrapped(*args, **kwargs) + if not done: + trysetip(instance, loglevel=logging.WARNING) + return result + + wrapt.wrap_function_wrapper( + http.client.HTTPConnection, + "send", + instrumented_send, + ) + + instrument_connect(http.client.HTTPConnection) + # No need to instrument HTTPSConnection, as it calls super().connect() + + +class _ConnectionState(TypedDict): + need_ip: list[Span] + + +def _getstate() -> _ConnectionState | None: + return cast(_ConnectionState, context.get_value(_STATE_KEY)) + + +@contextlib.contextmanager +def set_ip_on_next_http_connection(span: Span): + state = _getstate() + if not state: + token = context.attach( + context.set_value(_STATE_KEY, {"need_ip": [span]}) + ) + try: + yield + finally: + context.detach(token) + else: + spans = state["need_ip"] + spans.append(span) + try: + yield + finally: + try: + spans.remove(span) + except ValueError: # Span might have become non-recording + pass + + +def _uninstrument(): + unwrap(http.client.HTTPConnection, "send") + unwrap(http.client.HTTPConnection, "connect") diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/util/http/py.typed b/.venv/lib/python3.12/site-packages/opentelemetry/util/http/py.typed new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/opentelemetry/util/http/py.typed diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/util/http/version.py b/.venv/lib/python3.12/site-packages/opentelemetry/util/http/version.py new file mode 100644 index 00000000..7fb5b98b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/opentelemetry/util/http/version.py @@ -0,0 +1,15 @@ +# 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. + +__version__ = "0.52b1" |