diff --git a/datadog_checks_base/changelog.d/23849.fixed b/datadog_checks_base/changelog.d/23849.fixed new file mode 100644 index 0000000000000..c26df0cdb6a84 --- /dev/null +++ b/datadog_checks_base/changelog.d/23849.fixed @@ -0,0 +1 @@ +Fix ``resolve_db_host`` treating loopback IP literals (e.g. ``::1``) as DNS resolution failures, which caused database checks to submit metrics with the wrong host tag and miss agent host tags. diff --git a/datadog_checks_base/datadog_checks/base/utils/db/utils.py b/datadog_checks_base/datadog_checks/base/utils/db/utils.py index 6b371516cb6bd..5ad2a61a79d2f 100644 --- a/datadog_checks_base/datadog_checks/base/utils/db/utils.py +++ b/datadog_checks_base/datadog_checks/base/utils/db/utils.py @@ -12,7 +12,7 @@ import time from concurrent.futures.thread import ThreadPoolExecutor from enum import Enum, auto -from ipaddress import IPv4Address +from ipaddress import IPv4Address, IPv6Address, ip_address from typing import Any, Callable, Dict, List, Optional, Tuple, Union # noqa: F401 from cachetools import TTLCache @@ -162,12 +162,28 @@ def acquire(self, key): return True +def _try_parse_db_host_ip(db_host: str) -> IPv4Address | IPv6Address | None: + """Try to parse db_host as an IP address.""" + try: + return ip_address(db_host.strip()) + except ValueError: + return None + + +def _is_local_db_host(db_host: str | None) -> bool: + """Return True when the DB is reached via localhost, a unix socket, or a loopback IP.""" + if not db_host or db_host == 'localhost' or db_host.startswith('/'): + return True + addr = _try_parse_db_host_ip(db_host) + return addr is not None and addr.is_loopback + + def resolve_db_host(db_host): if db_host and db_host.endswith('.local'): return db_host agent_hostname = datadog_agent.get_hostname() - if not db_host or db_host in {'localhost', '127.0.0.1'} or db_host.startswith('/'): + if _is_local_db_host(db_host): return agent_hostname try: diff --git a/datadog_checks_base/tests/base/utils/db/test_util.py b/datadog_checks_base/tests/base/utils/db/test_util.py index dd3575fbd4b7f..d25e3b895c678 100644 --- a/datadog_checks_base/tests/base/utils/db/test_util.py +++ b/datadog_checks_base/tests/base/utils/db/test_util.py @@ -32,10 +32,25 @@ @pytest.mark.parametrize( "db_host, agent_hostname, want", [ + # Unset host or local hostname (None, "agent_hostname", "agent_hostname"), ("localhost", "agent_hostname", "agent_hostname"), + # Unix socket + ("/var/run/mysqld.sock", "agent_hostname", "agent_hostname"), + # Loopback IP literals (incl. zone-scoped IPv6) ("127.0.0.1", "agent_hostname", "agent_hostname"), + ("::1", "agent_hostname", "agent_hostname"), + ("::1%lo0", "agent_hostname", "agent_hostname"), + # Non-loopback IP literals keep the configured host + ("169.254.169.254", "agent_hostname", "169.254.169.254"), + ("fe80::1", "agent_hostname", "fe80::1"), + ("fe80::1%eth0", "agent_hostname", "fe80::1%eth0"), ("192.0.2.1", "agent_hostname", "192.0.2.1"), + ("2001:db8::1", "agent_hostname", "2001:db8::1"), + # Resolved DB host shares the agent host IP + ("192.0.2.1", "192.0.2.1", "192.0.2.1"), + ("192.0.2.1", "192.0.2.254", "192.0.2.1"), + # Hostname resolution failures fall back to the configured db_host ("socket.gaierror", "agent_hostname", "socket.gaierror"), ( "greater-than-or-equal-to-64-characters-causes-unicode-error-----", @@ -44,8 +59,7 @@ ), ("192.0.2.1", "socket.gaierror", "192.0.2.1"), ("192.0.2.1", "greater-than-or-equal-to-64-characters-causes-unicode-error-----", "192.0.2.1"), - ("192.0.2.1", "192.0.2.1", "192.0.2.1"), - ("192.0.2.1", "192.0.2.254", "192.0.2.1"), + # mDNS .local names are passed through unchanged ("postgres.svc.local", "some-pod", "postgres.svc.local"), ], )