Skip to content

Commit 37d623d

Browse files
New database instance identifier for Postges, MySQL, SQLServer (#19341) (#20088)
* Add unique database identifiers * Lint * WIP * WIP * WIP * WIP * Fix * Postgres identifier * WIP * WIP * Test fix * Mysql/SQLServer * Sort tags * Clean * Changelog * Fix PR numbers * Lint * Sync * WIP * Tests * Tests * WIP * WIP * WIP * WIP * MySQL * Lint * Get * Tag filter * Fix E2E * WIP * WIP * Tests * Lint * Docs (cherry picked from commit 76c4247) Co-authored-by: Seth Samuel <seth.samuel@datadoghq.com>
1 parent 27a20e1 commit 37d623d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+620
-211
lines changed

mysql/assets/configuration/spec.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,29 @@ files:
7070
and can be useful to set a custom hostname when connecting to a remote database through a proxy.
7171
value:
7272
type: string
73+
- name: database_identifier
74+
description: |
75+
Controls how the database is identified. The default value is the resolved hostname for the instance,
76+
which respects the `reported_hostname` option.
7377
78+
This value will be used as-is for the display name of the instance but will be normalized
79+
when applied as the `database_instance` tag. Please see https://docs.datadoghq.com/getting_started/tagging/
80+
for more details on Datadog tag normalization.
81+
options:
82+
- name: template
83+
value:
84+
type: string
85+
display_default: "$resolved_hostname"
86+
example: "$env-$resolved_hostname:$port"
87+
description: |
88+
The template to use for the database identifier. The default value is `$resolved_hostname`.
89+
You can use the following variables, prefixed by `$` in the template:
90+
- resolved_hostname: The resolved hostname of the instance, which respects the `reported_hostname` option.
91+
- host: The provided host of the instance.
92+
- port: The port number of the instance.
93+
- mysql_sock: The socket path of the instance.
94+
In addition, you can use any key from the `tags` section of the configuration.
95+
default: "$resolved_hostname"
7496
- name: sock
7597
description: |
7698
Path to a Unix Domain Socket to use when connecting to MySQL (instead of a TCP socket).

mysql/changelog.d/19341.added

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added a new configuration option `database_identifier.template`. Use this template to specify the unique identifier for a database instance, separate from the underlying host.
2+
The `empty_default_hostname` configuration option is now respected and will omit the `host` tag from database instances when enabled.

mysql/datadog_checks/mysql/activity.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ def run_job(self):
195195
'https://docs.datadoghq.com/database_monitoring/setup_mysql/troubleshooting#%s',
196196
DatabaseConfigurationError.events_waits_current_not_enabled.value,
197197
code=DatabaseConfigurationError.events_waits_current_not_enabled.value,
198-
host=self._check.resolved_hostname,
198+
host=self._check.reported_hostname,
199199
),
200200
)
201201
return
@@ -332,7 +332,7 @@ def _get_estimated_row_size_bytes(row):
332332
def _create_activity_event(self, active_sessions, tags):
333333
# type: (List[Dict[str]], List[Dict[str]]) -> Dict[str]
334334
return {
335-
"host": self._check.resolved_hostname,
335+
"host": self._check.reported_hostname,
336336
"ddagentversion": datadog_agent.get_version(),
337337
"ddsource": "mysql",
338338
"dbm_type": "activity",

mysql/datadog_checks/mysql/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
class MySQLConfig(object):
1313
def __init__(self, instance, init_config):
1414
self.log = get_check_logger()
15+
self.database_identifier = instance.get('database_identifier', {})
16+
self.empty_default_hostname = instance.get("empty_default_hostname", False)
1517
self.host = instance.get('host', instance.get('server', ''))
1618
self.port = int(instance.get('port', 0))
1719
self.reported_hostname = instance.get('reported_hostname', '')

mysql/datadog_checks/mysql/config_models/instance.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ class CustomQuery(BaseModel):
5858
tags: Optional[tuple[str, ...]] = None
5959

6060

61+
class DatabaseIdentifier(BaseModel):
62+
model_config = ConfigDict(
63+
arbitrary_types_allowed=True,
64+
frozen=True,
65+
)
66+
template: Optional[str] = None
67+
68+
6169
class Gcp(BaseModel):
6270
model_config = ConfigDict(
6371
arbitrary_types_allowed=True,
@@ -200,6 +208,7 @@ class InstanceConfig(BaseModel):
200208
collect_settings: Optional[CollectSettings] = None
201209
connect_timeout: Optional[float] = None
202210
custom_queries: Optional[tuple[CustomQuery, ...]] = None
211+
database_identifier: Optional[DatabaseIdentifier] = None
203212
database_instance_collection_interval: Optional[float] = None
204213
dbm: Optional[bool] = None
205214
defaults_file: Optional[str] = None

mysql/datadog_checks/mysql/data/conf.yaml.example

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,26 @@ instances:
6464
#
6565
# reported_hostname: <REPORTED_HOSTNAME>
6666

67+
## Controls how the database is identified. The default value is the resolved hostname for the instance,
68+
## which respects the `reported_hostname` option.
69+
##
70+
## This value will be used as-is for the display name of the instance but will be normalized
71+
## when applied as the `database_instance` tag. Please see https://docs.datadoghq.com/getting_started/tagging/
72+
## for more details on Datadog tag normalization.
73+
#
74+
# database_identifier:
75+
76+
## @param template - string - optional - default: $resolved_hostname
77+
## The template to use for the database identifier. The default value is `$resolved_hostname`.
78+
## You can use the following variables, prefixed by `$` in the template:
79+
## - resolved_hostname: The resolved hostname of the instance, which respects the `reported_hostname` option.
80+
## - host: The provided host of the instance.
81+
## - port: The port number of the instance.
82+
## - mysql_sock: The socket path of the instance.
83+
## In addition, you can use any key from the `tags` section of the configuration.
84+
#
85+
# template: $env-$resolved_hostname:$port
86+
6787
## @param sock - string - optional
6888
## Path to a Unix Domain Socket to use when connecting to MySQL (instead of a TCP socket).
6989
## If you specify a socket you dont need to specify a host.

mysql/datadog_checks/mysql/databases_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def _cursor_run(self, cursor, query, params=None):
157157
"dd.mysql.db.error",
158158
1,
159159
tags=self._tags + ["error:{}".format(type(e))] + self._check._get_debug_tags(),
160-
hostname=self._check.resolved_hostname,
160+
hostname=self._check.reported_hostname,
161161
)
162162
raise
163163

mysql/datadog_checks/mysql/metadata.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def _cursor_run(self, cursor, query, params=None):
118118
"dd.mysql.db.error",
119119
1,
120120
tags=self._tags + ["error:{}".format(type(e))] + self._check._get_debug_tags(),
121-
hostname=self._check.resolved_hostname,
121+
hostname=self._check.reported_hostname,
122122
)
123123
raise
124124

@@ -169,7 +169,7 @@ def report_mysql_metadata(self):
169169
rows = cursor.fetchall()
170170
settings = [dict(row) for row in rows]
171171
event = {
172-
"host": self._check.resolved_hostname,
172+
"host": self._check.reported_hostname,
173173
"agent_version": datadog_agent.get_version(),
174174
"dbms": "mysql",
175175
"kind": "mysql_variables",

mysql/datadog_checks/mysql/mysql.py

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import traceback
1010
from collections import defaultdict
1111
from contextlib import closing, contextmanager
12+
from string import Template
1213
from typing import Any, Dict, List, Optional # noqa: F401
1314

1415
import pymysql
@@ -109,11 +110,13 @@ def __init__(self, name, init_config, instances):
109110
self.version = None
110111
self.is_mariadb = None
111112
self._resolved_hostname = None
113+
self._database_identifier = None
112114
self._agent_hostname = None
113115
self._database_hostname = None
114116
self._is_aurora = None
115117
self._config = MySQLConfig(self.instance, init_config)
116118
self.tags = self._config.tags
119+
self.add_core_tags()
117120
self.cloud_metadata = self._config.cloud_metadata
118121

119122
# Create a new connection on every check run
@@ -157,15 +160,46 @@ def _send_metadata(self):
157160
self.set_metadata('flavor', self.version.flavor)
158161
self.set_metadata('resolved_hostname', self.resolved_hostname)
159162

163+
@property
164+
def reported_hostname(self):
165+
# type: () -> str
166+
if self._config.empty_default_hostname:
167+
return None
168+
return self.resolved_hostname
169+
160170
@property
161171
def resolved_hostname(self):
172+
# type: () -> str
162173
if self._resolved_hostname is None:
163174
if self._config.reported_hostname:
164175
self._resolved_hostname = self._config.reported_hostname
165176
else:
166177
self._resolved_hostname = self.resolve_db_host()
167178
return self._resolved_hostname
168179

180+
@property
181+
def database_identifier(self):
182+
# type: () -> str
183+
if self._database_identifier is None:
184+
template = Template(self._config.database_identifier.get('template') or '$resolved_hostname')
185+
tag_dict = {}
186+
tags = self.tags.copy()
187+
# sort tags to ensure consistent ordering
188+
tags.sort()
189+
for t in tags:
190+
if ':' in t:
191+
key, value = t.split(':', 1)
192+
if key in tag_dict:
193+
tag_dict[key] += f",{value}"
194+
else:
195+
tag_dict[key] = value
196+
tag_dict['resolved_hostname'] = self.resolved_hostname
197+
tag_dict['host'] = str(self._config.host)
198+
tag_dict['port'] = str(self._config.port)
199+
tag_dict['mysql_sock'] = str(self._config.mysql_sock)
200+
self._database_identifier = template.safe_substitute(**tag_dict)
201+
return self._database_identifier
202+
169203
@property
170204
def agent_hostname(self):
171205
# type: () -> str
@@ -180,9 +214,14 @@ def database_hostname(self):
180214
self._database_hostname = self.resolve_db_host()
181215
return self._database_hostname
182216

183-
def set_resource_tags(self):
217+
def add_core_tags(self):
218+
"""
219+
Add tags that should be attached to every metric/event but which require check calculations outside the config.
220+
"""
184221
self.tags.append("database_hostname:{}".format(self.database_hostname))
222+
self.tags.append("database_instance:{}".format(self.database_identifier))
185223

224+
def set_resource_tags(self):
186225
if self.cloud_metadata.get("gcp") is not None:
187226
self.tags.append(
188227
"dd.internal.resource:gcp_sql_database_instance:{}:{}".format(
@@ -199,6 +238,9 @@ def set_resource_tags(self):
199238
# allow for detecting if the host is an RDS host, and emit
200239
# the resource properly even if the `aws` config is unset
201240
self.tags.append("dd.internal.resource:aws_rds_instance:{}".format(self.resolved_hostname))
241+
self.cloud_metadata["aws"] = {
242+
"instance_endpoint": self.resolved_hostname,
243+
}
202244
if self.cloud_metadata.get("azure") is not None:
203245
deployment_type = self.cloud_metadata.get("azure")["deployment_type"]
204246
# some `deployment_type`s map to multiple `resource_type`s
@@ -210,7 +252,7 @@ def set_resource_tags(self):
210252
# finally, emit a `database_instance` resource for this instance
211253
self.tags.append(
212254
"dd.internal.resource:database_instance:{}".format(
213-
self.resolved_hostname,
255+
self.database_identifier,
214256
)
215257
)
216258

@@ -371,7 +413,7 @@ def _new_query_executor(self, queries):
371413
self.execute_query_raw,
372414
self,
373415
queries=queries,
374-
hostname=self.resolved_hostname,
416+
hostname=self.reported_hostname,
375417
track_operation_time=True,
376418
)
377419

@@ -467,12 +509,12 @@ def _connect(self):
467509
self.log.debug("Connected to MySQL")
468510
self.service_check_tags = list(set(service_check_tags))
469511
self.service_check(
470-
self.SERVICE_CHECK_NAME, AgentCheck.OK, tags=service_check_tags, hostname=self.resolved_hostname
512+
self.SERVICE_CHECK_NAME, AgentCheck.OK, tags=service_check_tags, hostname=self.reported_hostname
471513
)
472514
yield db
473515
except Exception:
474516
self.service_check(
475-
self.SERVICE_CHECK_NAME, AgentCheck.CRITICAL, tags=service_check_tags, hostname=self.resolved_hostname
517+
self.SERVICE_CHECK_NAME, AgentCheck.CRITICAL, tags=service_check_tags, hostname=self.reported_hostname
476518
)
477519
raise
478520
finally:
@@ -796,20 +838,20 @@ def _submit_replication_status(self, status, additional_tags):
796838
name=self.SLAVE_SERVICE_CHECK_NAME,
797839
value=1 if status == AgentCheck.OK else 0,
798840
tags=self.tags + additional_tags,
799-
hostname=self.resolved_hostname,
841+
hostname=self.reported_hostname,
800842
)
801843
# deprecated in favor of service_check("mysql.replication.replica_running")
802844
self.service_check(
803845
self.SLAVE_SERVICE_CHECK_NAME,
804846
status,
805847
tags=self.service_check_tags + additional_tags,
806-
hostname=self.resolved_hostname,
848+
hostname=self.reported_hostname,
807849
)
808850
self.service_check(
809851
self.REPLICA_SERVICE_CHECK_NAME,
810852
status,
811853
tags=self.service_check_tags + additional_tags,
812-
hostname=self.resolved_hostname,
854+
hostname=self.reported_hostname,
813855
)
814856

815857
def _is_source_host(self, replicas, results):
@@ -858,13 +900,13 @@ def __submit_metric(self, metric_name, metric_type, variable, db_results, tags):
858900
metric_tags.append(tag)
859901
if value is not None:
860902
if metric_type == RATE:
861-
self.rate(metric_name, value, tags=metric_tags, hostname=self.resolved_hostname)
903+
self.rate(metric_name, value, tags=metric_tags, hostname=self.reported_hostname)
862904
elif metric_type == GAUGE:
863-
self.gauge(metric_name, value, tags=metric_tags, hostname=self.resolved_hostname)
905+
self.gauge(metric_name, value, tags=metric_tags, hostname=self.reported_hostname)
864906
elif metric_type == COUNT:
865-
self.count(metric_name, value, tags=metric_tags, hostname=self.resolved_hostname)
907+
self.count(metric_name, value, tags=metric_tags, hostname=self.reported_hostname)
866908
elif metric_type == MONOTONIC:
867-
self.monotonic_count(metric_name, value, tags=metric_tags, hostname=self.resolved_hostname)
909+
self.monotonic_count(metric_name, value, tags=metric_tags, hostname=self.reported_hostname)
868910

869911
def _collect_dict(self, metric_type, field_metric_map, query, db, tags):
870912
"""
@@ -891,15 +933,15 @@ def _collect_dict(self, metric_type, field_metric_map, query, db, tags):
891933
self.log.debug("Collecting done, value %s", result[col_idx])
892934
if metric_type == GAUGE:
893935
self.gauge(
894-
metric, float(result[col_idx]), tags=tags, hostname=self.resolved_hostname
936+
metric, float(result[col_idx]), tags=tags, hostname=self.reported_hostname
895937
)
896938
elif metric_type == RATE:
897939
self.rate(
898-
metric, float(result[col_idx]), tags=tags, hostname=self.resolved_hostname
940+
metric, float(result[col_idx]), tags=tags, hostname=self.reported_hostname
899941
)
900942
else:
901943
self.gauge(
902-
metric, float(result[col_idx]), tags=tags, hostname=self.resolved_hostname
944+
metric, float(result[col_idx]), tags=tags, hostname=self.reported_hostname
903945
)
904946
else:
905947
self.log.debug("Received value is None for index %d", col_idx)
@@ -956,10 +998,10 @@ def _collect_system_metrics(self, host, db, tags):
956998
scpu = proc.cpu_times()[1]
957999

9581000
if ucpu and scpu:
959-
self.rate("mysql.performance.user_time", ucpu, tags=tags, hostname=self.resolved_hostname)
1001+
self.rate("mysql.performance.user_time", ucpu, tags=tags, hostname=self.reported_hostname)
9601002
# should really be system_time
961-
self.rate("mysql.performance.kernel_time", scpu, tags=tags, hostname=self.resolved_hostname)
962-
self.rate("mysql.performance.cpu_time", ucpu + scpu, tags=tags, hostname=self.resolved_hostname)
1003+
self.rate("mysql.performance.kernel_time", scpu, tags=tags, hostname=self.reported_hostname)
1004+
self.rate("mysql.performance.cpu_time", ucpu + scpu, tags=tags, hostname=self.reported_hostname)
9631005
else:
9641006
self.log.debug("psutil is not available, will not collect mysql.performance.* metrics")
9651007
except Exception:
@@ -1334,10 +1376,11 @@ def _report_warnings(self):
13341376
self.warning(warning)
13351377

13361378
def _send_database_instance_metadata(self):
1337-
if self.resolved_hostname not in self._database_instance_emitted:
1379+
if self.database_identifier not in self._database_instance_emitted:
13381380
event = {
1339-
"host": self.resolved_hostname,
1381+
"host": self.reported_hostname,
13401382
"port": self._config.port,
1383+
"database_instance": self.database_identifier,
13411384
"database_hostname": self.database_hostname,
13421385
"agent_version": datadog_agent.get_version(),
13431386
"dbms": "mysql",
@@ -1353,5 +1396,5 @@ def _send_database_instance_metadata(self):
13531396
"connection_host": self._config.host,
13541397
},
13551398
}
1356-
self._database_instance_emitted[self.resolved_hostname] = event
1399+
self._database_instance_emitted[self.database_identifier] = event
13571400
self.database_monitoring_metadata(json.dumps(event, default=default_json_event_encoding))

0 commit comments

Comments
 (0)