diff --git a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/base.py b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/base.py index 1a476138157de..6552a1723c4d1 100644 --- a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/base.py +++ b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/base.py @@ -8,6 +8,7 @@ from datadog_checks.base.checks import AgentCheck from datadog_checks.base.errors import ConfigurationError +from datadog_checks.base.utils.http_exceptions import HTTPRequestError, HTTPStatusError from datadog_checks.base.utils.tracing import traced_class from .scraper import OpenMetricsScraper @@ -71,7 +72,8 @@ def check(self, _): with self.adopt_namespace(scraper.namespace): try: scraper.scrape() - except (ConnectionError, RequestException) as e: + # Pairs requests-native + library-agnostic exceptions; simplify to HTTPError after migration. + except (ConnectionError, RequestException, HTTPRequestError, HTTPStatusError) as e: self.log.error("There was an error scraping endpoint %s: %s", endpoint, str(e)) raise type(e)("There was an error scraping endpoint {}: {}".format(endpoint, e)) from None diff --git a/datadog_checks_base/datadog_checks/base/utils/http_protocol.py b/datadog_checks_base/datadog_checks/base/utils/http_protocol.py index f6fb97ca5c849..4d103de5930c3 100644 --- a/datadog_checks_base/datadog_checks/base/utils/http_protocol.py +++ b/datadog_checks_base/datadog_checks/base/utils/http_protocol.py @@ -13,6 +13,11 @@ class HTTPResponseProtocol(Protocol): text: str headers: Mapping[str, str] + @property + def ok(self) -> bool: ... + @property + def reason(self) -> str: ... + def json(self, **kwargs: Any) -> Any: ... def raise_for_status(self) -> None: ... def close(self) -> None: ... diff --git a/datadog_checks_base/datadog_checks/base/utils/http_testing.py b/datadog_checks_base/datadog_checks/base/utils/http_testing.py index 3a509f2673f3f..83b6a85c263d2 100644 --- a/datadog_checks_base/datadog_checks/base/utils/http_testing.py +++ b/datadog_checks_base/datadog_checks/base/utils/http_testing.py @@ -3,7 +3,9 @@ # Licensed under a 3-clause BSD style license (see LICENSE) import json from datetime import timedelta +from http.client import responses as http_responses from io import BytesIO +from textwrap import dedent from typing import Any, Iterator from unittest.mock import MagicMock @@ -41,7 +43,7 @@ def __init__( (isinstance(content, str) and content.startswith('\n')) or (isinstance(content, bytes) and content.startswith(b'\n')) ): - content = content[1:] + content = dedent(content[1:]) if isinstance(content, str) else content[1:] self._content = content.encode('utf-8') if isinstance(content, str) else content self.status_code = status_code @@ -62,6 +64,16 @@ def content(self) -> bytes: def text(self) -> str: return self._content.decode('utf-8') + @property + def ok(self) -> bool: + # Transitional: mirrors requests.Response.ok for current production code. + # httpx uses is_success/is_client_error/is_server_error instead. + return self.status_code < 400 + + @property + def reason(self) -> str: + return http_responses.get(self.status_code, '') + def json(self, **kwargs: Any) -> Any: return json.loads(self.text, **kwargs) diff --git a/datadog_checks_base/tests/base/utils/http/test_http_testing.py b/datadog_checks_base/tests/base/utils/http/test_http_testing.py index 81d49bc17b653..32a9036db7080 100644 --- a/datadog_checks_base/tests/base/utils/http/test_http_testing.py +++ b/datadog_checks_base/tests/base/utils/http/test_http_testing.py @@ -70,3 +70,25 @@ def test_mock_response_normalize_leading_newline(): response = MockHTTPResponse(content=content) assert response.text == 'Actual content' + + +def test_mock_response_normalize_leading_newline_with_indent(): + content = """ + line one + line two + """ + response = MockHTTPResponse(content=content) + assert response.text == "line one\nline two\n" + + +def test_mock_response_ok_property(): + assert MockHTTPResponse(status_code=200).ok is True + assert MockHTTPResponse(status_code=399).ok is True + assert MockHTTPResponse(status_code=400).ok is False + assert MockHTTPResponse(status_code=500).ok is False + + +def test_mock_response_reason_property(): + assert MockHTTPResponse(status_code=200).reason == 'OK' + assert MockHTTPResponse(status_code=404).reason == 'Not Found' + assert MockHTTPResponse(status_code=999).reason == '' diff --git a/traefik_mesh/changelog.d/22676.changed b/traefik_mesh/changelog.d/22676.changed new file mode 100644 index 0000000000000..f39347eb80c40 --- /dev/null +++ b/traefik_mesh/changelog.d/22676.changed @@ -0,0 +1 @@ +Remove unused ``url`` parameter from ``get_version``. \ No newline at end of file diff --git a/traefik_mesh/datadog_checks/traefik_mesh/check.py b/traefik_mesh/datadog_checks/traefik_mesh/check.py index a1e1549f9c566..ad1394c8a9aca 100644 --- a/traefik_mesh/datadog_checks/traefik_mesh/check.py +++ b/traefik_mesh/datadog_checks/traefik_mesh/check.py @@ -8,6 +8,9 @@ import requests from datadog_checks.base import AgentCheck, OpenMetricsBaseCheckV2 +from datadog_checks.base.utils.http_exceptions import HTTPConnectionError as _HTTPConnectionError +from datadog_checks.base.utils.http_exceptions import HTTPStatusError +from datadog_checks.base.utils.http_exceptions import HTTPTimeoutError as _HTTPTimeoutError from datadog_checks.traefik_mesh.config_models import ConfigMixin from datadog_checks.traefik_mesh.metrics import METRIC_MAP, RENAME_LABELS @@ -84,7 +87,7 @@ def get_mesh_ready_status(self): return node_status - def get_version(self, url): + def get_version(self): """Fetches Traefik Proxy version from the Proxy API""" version_url = urljoin(self.traefik_proxy_api_endpoint, PROXY_VERSION) @@ -112,10 +115,15 @@ def _get_json(self, url): resp = self.http.get(url) resp.raise_for_status() return resp.json() - except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError) as e: + except ( + requests.exceptions.HTTPError, + requests.exceptions.ConnectionError, + HTTPStatusError, + _HTTPConnectionError, + ) as e: self.warning( "Couldn't connect to URL: %s with exception: %s. Please verify the address is reachable", url, e ) - except requests.exceptions.Timeout as e: + except (requests.exceptions.Timeout, _HTTPTimeoutError) as e: self.warning("Connection timeout when connecting to %s: %s", url, e) return None diff --git a/traefik_mesh/tests/test_unit.py b/traefik_mesh/tests/test_unit.py index 6a7cb29d2507f..c6c3acbc697df 100644 --- a/traefik_mesh/tests/test_unit.py +++ b/traefik_mesh/tests/test_unit.py @@ -5,6 +5,9 @@ import pytest from datadog_checks.base.constants import ServiceCheck +from datadog_checks.base.utils.http_exceptions import HTTPConnectionError as _HTTPConnectionError +from datadog_checks.base.utils.http_exceptions import HTTPStatusError +from datadog_checks.base.utils.http_exceptions import HTTPTimeoutError as _HTTPTimeoutError from datadog_checks.dev.utils import assert_service_checks, get_metadata_metrics from datadog_checks.traefik_mesh import TraefikMeshCheck @@ -125,3 +128,21 @@ def test_submit_version(datadog_agent, dd_run_check, mock_http_response): } datadog_agent.assert_metadata('test:123', version_metadata) + + +def test_get_json_handles_http_status_error(): + check = TraefikMeshCheck('traefik_mesh', {}, [OM_MOCKED_INSTANCE]) + with mock.patch('requests.Session.get', side_effect=HTTPStatusError('404 Client Error')): + assert check._get_json('http://example.com/api') is None + + +def test_get_json_handles_http_connection_error(): + check = TraefikMeshCheck('traefik_mesh', {}, [OM_MOCKED_INSTANCE]) + with mock.patch('requests.Session.get', side_effect=_HTTPConnectionError('Connection refused')): + assert check._get_json('http://example.com/api') is None + + +def test_get_json_handles_http_timeout_error(): + check = TraefikMeshCheck('traefik_mesh', {}, [OM_MOCKED_INSTANCE]) + with mock.patch('requests.Session.get', side_effect=_HTTPTimeoutError('Read timed out')): + assert check._get_json('http://example.com/api') is None