Skip to content

Commit d8e7b90

Browse files
authored
[DBMON-6785] add support for database_identifier templating to DatabaseCheck (#24250)
* Implement agent_hostname in DatabaseCheck * Add changelog * add support for database_identifier templating to DatabaseCheck * Add changelog
1 parent 30c733c commit d8e7b90

3 files changed

Lines changed: 125 additions & 10 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Provide a default `database_identifier` implementation on the `DatabaseCheck` base class that is built (and cached) from the `database_identifier_template` and `database_identifier_params` hooks, so integrations no longer need to reimplement the database identifier templating logic.

datadog_checks_base/datadog_checks/base/checks/db.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Licensed under a 3-clause BSD style license (see LICENSE)
44

55
from abc import abstractmethod
6+
from string import Template
67

78
from datadog_checks.base.agent import datadog_agent
89
from datadog_checks.base.utils.db.utils import TagManager
@@ -14,6 +15,7 @@ class DatabaseCheck(AgentCheck):
1415
def __init__(self, *args, **kwargs):
1516
super().__init__(*args, **kwargs)
1617
self._agent_hostname = None
18+
self._database_identifier = None
1719
self.tag_manager = TagManager()
1820

1921
def database_monitoring_query_sample(self, raw_event: str):
@@ -37,9 +39,68 @@ def reported_hostname(self) -> str | None:
3739
pass
3840

3941
@property
40-
@abstractmethod
4142
def database_identifier(self) -> str:
42-
pass
43+
"""
44+
The unique identifier for this database instance, used as the ``database_instance`` tag and
45+
in DBM metadata payloads.
46+
47+
The value is built once (and cached) from :attr:`database_identifier_template` and
48+
:attr:`database_identifier_params` via :meth:`_build_database_identifier`. Integrations
49+
customize the result by overriding those two hooks instead of reimplementing the templating
50+
logic.
51+
"""
52+
if self._database_identifier is None:
53+
self._database_identifier = self._build_database_identifier(
54+
self.database_identifier_template,
55+
self.database_identifier_params,
56+
)
57+
return self._database_identifier
58+
59+
@property
60+
def database_identifier_template(self) -> str:
61+
"""
62+
The ``string.Template``-style template used to build :attr:`database_identifier`.
63+
64+
Defaults to ``"$resolved_hostname"``. Integrations typically override this to return the
65+
template from their configuration (e.g. ``self._config.database_identifier.template``).
66+
"""
67+
return "$resolved_hostname"
68+
69+
@property
70+
def database_identifier_params(self) -> dict:
71+
"""
72+
Connection-derived values exposed to :attr:`database_identifier_template`.
73+
74+
These are applied after tags, so they take precedence over any tag of the same name.
75+
Values are stringified by the template engine, so callers need not cast them. Defaults to an
76+
empty mapping.
77+
"""
78+
return {}
79+
80+
def _build_database_identifier(self, template: str, connection_params: dict | None = None) -> str:
81+
"""
82+
Build a database identifier string from a template and the check's tags.
83+
84+
Each ``key:value`` tag is exposed to the template as a ``$key`` variable, with duplicate
85+
keys joined by commas (after sorting tags for a stable ordering). The ``connection_params``
86+
mapping is applied last so connection-derived values (e.g. ``resolved_hostname``, ``host``,
87+
``port``) take precedence over any tag of the same name.
88+
89+
:param template: A ``string.Template``-style template, e.g. ``"$resolved_hostname"``.
90+
:param connection_params: Optional mapping of additional template variables.
91+
:return: The substituted identifier. Unknown ``$variables`` are left intact.
92+
"""
93+
tag_dict: dict[str, str] = {}
94+
for tag in sorted(self.tags):
95+
if ':' in tag:
96+
key, value = tag.split(':', 1)
97+
if key in tag_dict:
98+
tag_dict[key] += f",{value}"
99+
else:
100+
tag_dict[key] = value
101+
if connection_params:
102+
tag_dict.update(connection_params)
103+
return Template(template).safe_substitute(**tag_dict)
43104

44105
@property
45106
def agent_hostname(self) -> str:

datadog_checks_base/tests/base/checks/test_database_check.py

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# Licensed under a 3-clause BSD style license (see LICENSE)
44
from unittest import mock
55

6+
import pytest
7+
68
from datadog_checks.base.checks.db import DatabaseCheck
79
from datadog_checks.base.stubs.datadog_agent import datadog_agent
810

@@ -12,18 +14,10 @@ class FakeDatabaseCheck(DatabaseCheck):
1214
def reported_hostname(self):
1315
return None
1416

15-
@property
16-
def database_identifier(self):
17-
return "test-db"
18-
1917
@property
2018
def dbms_version(self):
2119
return "1.0.0"
2220

23-
@property
24-
def tags(self):
25-
return []
26-
2721
@property
2822
def cloud_metadata(self):
2923
return {}
@@ -36,3 +30,62 @@ def test_agent_hostname_resolves_once_and_caches():
3630
assert check.agent_hostname == "my-agent-host"
3731
assert check.agent_hostname == "my-agent-host"
3832
assert get_hostname.call_count == 1
33+
34+
35+
@pytest.mark.parametrize(
36+
("tags", "template", "connection_params", "expected"),
37+
[
38+
pytest.param([], "$resolved_hostname", {"resolved_hostname": "my-host"}, "my-host", id="connection_params"),
39+
pytest.param(["env:prod"], "$env", None, "prod", id="substitutes_tags"),
40+
# Tags are sorted before merging, so the result is deterministic regardless of input order.
41+
pytest.param(["team:b", "team:a"], "$team", None, "a,b", id="merges_duplicate_keys_sorted"),
42+
pytest.param(["host:tag-host"], "$host", {"host": "conn-host"}, "conn-host", id="connection_params_override"),
43+
# Tags without a ':' are not exposed as template variables.
44+
pytest.param(["keyless"], "$keyless", None, "$keyless", id="ignores_keyless_tags"),
45+
pytest.param([], "$missing", None, "$missing", id="unknown_variables_intact"),
46+
],
47+
)
48+
def test_build_database_identifier(tags, template, connection_params, expected):
49+
check = FakeDatabaseCheck("test", {}, [{}])
50+
check.tag_manager.set_tags_from_list(tags)
51+
assert check._build_database_identifier(template, connection_params) == expected
52+
53+
54+
@pytest.mark.parametrize(
55+
("template", "params", "tags", "tags_after", "expected"),
56+
[
57+
# template=None / params=None delegate to the base hooks (default template "$resolved_hostname").
58+
pytest.param(None, None, ["resolved_hostname:my-host"], None, "my-host", id="default_template_uses_tags"),
59+
# Non-string param values are stringified by the template engine, so no casting is needed.
60+
pytest.param("$host:$port", {"host": "db-host", "port": 5432}, [], None, "db-host:5432", id="overridden_hooks"),
61+
# Connection params take precedence over tags of the same name.
62+
pytest.param(
63+
"$host:$port",
64+
{"host": "db-host", "port": 5432},
65+
["host:tag-host", "port:1111"],
66+
None,
67+
"db-host:5432",
68+
id="params_override_tags",
69+
),
70+
# Mutating tags after first access has no effect: the identifier is built once and cached.
71+
pytest.param(
72+
None, None, ["resolved_hostname:first"], ["resolved_hostname:second"], "first", id="built_once_and_cached"
73+
),
74+
],
75+
)
76+
def test_database_identifier(template, params, tags, tags_after, expected):
77+
class EmbeddedDatabaseCheck(FakeDatabaseCheck):
78+
@property
79+
def database_identifier_template(self):
80+
return super().database_identifier_template if template is None else template
81+
82+
@property
83+
def database_identifier_params(self):
84+
return super().database_identifier_params if params is None else params
85+
86+
check = EmbeddedDatabaseCheck("test", {}, [{}])
87+
check.tag_manager.set_tags_from_list(tags)
88+
assert check.database_identifier == expected
89+
if tags_after is not None:
90+
check.tag_manager.set_tags_from_list(tags_after, replace=True)
91+
assert check.database_identifier == expected

0 commit comments

Comments
 (0)