From 80c7783c9fdcfe96e87604480aa7387e2fb6b97e Mon Sep 17 00:00:00 2001 From: Eric Weaver Date: Wed, 27 May 2026 10:04:31 -0400 Subject: [PATCH 1/4] Fix resolve_db_host for local IPv6 and link-local addresses --- .../datadog_checks/base/utils/db/utils.py | 28 +++++++++++++++++-- .../tests/base/utils/db/test_util.py | 20 +++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) 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..9b93805853fa1 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, IPv6Interface, ip_address from typing import Any, Callable, Dict, List, Optional, Tuple, Union # noqa: F401 from cachetools import TTLCache @@ -162,12 +162,36 @@ 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""" + host = db_host.strip() + if host.startswith('[') and host.endswith(']'): + host = host[1:-1] + if '%' in host: + try: + return IPv6Interface(host).ip + except ValueError: + return None + try: + return ip_address(host) + 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 loopback/link-local 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 or addr.is_link_local) + + 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..62758d04be4a1 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,27 @@ @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"), + # IPv4 loopback and link-local literals ("127.0.0.1", "agent_hostname", "agent_hostname"), + ("169.254.169.254", "agent_hostname", "agent_hostname"), + # IPv6 loopback and link-local literals (incl. bracketed and zone-scoped forms) + ("::1", "agent_hostname", "agent_hostname"), + ("[::1]", "agent_hostname", "agent_hostname"), + ("fe80::1", "agent_hostname", "agent_hostname"), + ("[fe80::1]", "agent_hostname", "agent_hostname"), + ("fe80::1%lo0", "agent_hostname", "agent_hostname"), + # Remote IP literals ("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 +61,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"), ], ) From a024710de9cb46cfdbee506a0e352c7cfbc98ffb Mon Sep 17 00:00:00 2001 From: Eric Weaver Date: Wed, 27 May 2026 10:14:59 -0400 Subject: [PATCH 2/4] Add changelog --- datadog_checks_base/changelog.d/23849.fixed | 1 + 1 file changed, 1 insertion(+) create mode 100644 datadog_checks_base/changelog.d/23849.fixed diff --git a/datadog_checks_base/changelog.d/23849.fixed b/datadog_checks_base/changelog.d/23849.fixed new file mode 100644 index 0000000000000..4c92e082099a0 --- /dev/null +++ b/datadog_checks_base/changelog.d/23849.fixed @@ -0,0 +1 @@ +Fix ``resolve_db_host`` treating loopback and link-local 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. From 9badf28b6aa16743ae46bd9ece1cd0141d99dbd1 Mon Sep 17 00:00:00 2001 From: Eric Weaver Date: Wed, 27 May 2026 10:28:47 -0400 Subject: [PATCH 3/4] Narrow changes to loopback addresses only --- datadog_checks_base/changelog.d/23849.fixed | 2 +- .../datadog_checks/base/utils/db/utils.py | 4 ++-- datadog_checks_base/tests/base/utils/db/test_util.py | 11 ++++------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/datadog_checks_base/changelog.d/23849.fixed b/datadog_checks_base/changelog.d/23849.fixed index 4c92e082099a0..c26df0cdb6a84 100644 --- a/datadog_checks_base/changelog.d/23849.fixed +++ b/datadog_checks_base/changelog.d/23849.fixed @@ -1 +1 @@ -Fix ``resolve_db_host`` treating loopback and link-local 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. +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 9b93805853fa1..0deaa42713aee 100644 --- a/datadog_checks_base/datadog_checks/base/utils/db/utils.py +++ b/datadog_checks_base/datadog_checks/base/utils/db/utils.py @@ -179,11 +179,11 @@ def _try_parse_db_host_ip(db_host: str) -> IPv4Address | IPv6Address | None: def _is_local_db_host(db_host: str | None) -> bool: - """Return True when the DB is reached via localhost, a unix socket, or loopback/link-local IP.""" + """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 or addr.is_link_local) + return addr is not None and addr.is_loopback def resolve_db_host(db_host): 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 62758d04be4a1..eea83b512f296 100644 --- a/datadog_checks_base/tests/base/utils/db/test_util.py +++ b/datadog_checks_base/tests/base/utils/db/test_util.py @@ -37,16 +37,13 @@ ("localhost", "agent_hostname", "agent_hostname"), # Unix socket ("/var/run/mysqld.sock", "agent_hostname", "agent_hostname"), - # IPv4 loopback and link-local literals + # Loopback IP literals (incl. bracketed IPv6) ("127.0.0.1", "agent_hostname", "agent_hostname"), - ("169.254.169.254", "agent_hostname", "agent_hostname"), - # IPv6 loopback and link-local literals (incl. bracketed and zone-scoped forms) ("::1", "agent_hostname", "agent_hostname"), ("[::1]", "agent_hostname", "agent_hostname"), - ("fe80::1", "agent_hostname", "agent_hostname"), - ("[fe80::1]", "agent_hostname", "agent_hostname"), - ("fe80::1%lo0", "agent_hostname", "agent_hostname"), - # Remote IP literals + # Link-local and remote IP literals keep the configured host + ("169.254.169.254", "agent_hostname", "169.254.169.254"), + ("fe80::1", "agent_hostname", "fe80::1"), ("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 From 56d50311a3e509ed91d1834260eae75e8c285dd6 Mon Sep 17 00:00:00 2001 From: Eric Weaver Date: Fri, 29 May 2026 12:38:38 -0400 Subject: [PATCH 4/4] review feedback changes --- .../datadog_checks/base/utils/db/utils.py | 14 +++----------- .../tests/base/utils/db/test_util.py | 7 ++++--- 2 files changed, 7 insertions(+), 14 deletions(-) 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 0deaa42713aee..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, IPv6Address, IPv6Interface, ip_address +from ipaddress import IPv4Address, IPv6Address, ip_address from typing import Any, Callable, Dict, List, Optional, Tuple, Union # noqa: F401 from cachetools import TTLCache @@ -163,17 +163,9 @@ def acquire(self, key): def _try_parse_db_host_ip(db_host: str) -> IPv4Address | IPv6Address | None: - """Try to parse db_host as an IP address""" - host = db_host.strip() - if host.startswith('[') and host.endswith(']'): - host = host[1:-1] - if '%' in host: - try: - return IPv6Interface(host).ip - except ValueError: - return None + """Try to parse db_host as an IP address.""" try: - return ip_address(host) + return ip_address(db_host.strip()) except ValueError: return None 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 eea83b512f296..d25e3b895c678 100644 --- a/datadog_checks_base/tests/base/utils/db/test_util.py +++ b/datadog_checks_base/tests/base/utils/db/test_util.py @@ -37,13 +37,14 @@ ("localhost", "agent_hostname", "agent_hostname"), # Unix socket ("/var/run/mysqld.sock", "agent_hostname", "agent_hostname"), - # Loopback IP literals (incl. bracketed IPv6) + # Loopback IP literals (incl. zone-scoped IPv6) ("127.0.0.1", "agent_hostname", "agent_hostname"), ("::1", "agent_hostname", "agent_hostname"), - ("[::1]", "agent_hostname", "agent_hostname"), - # Link-local and remote IP literals keep the configured host + ("::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