Skip to content

Commit 778d9c7

Browse files
Emit index usage and index size metrics from mysql integration (#19383)
* Emit index usage and size metrics * Add logs for debugging * Use query executor to run index usage queries * Set collection interval to 5 min * linter, change log, and config validation * clean up * sort metadata.csv * sync models * magic number replaced with var * linter * make collection interval configurable * fix spec.yaml * sync models * update readme, report size in bytes, don't exclude primary * updated spec description
1 parent f184aa2 commit 778d9c7

File tree

12 files changed

+202
-4
lines changed

12 files changed

+202
-4
lines changed

mysql/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ mysql> GRANT SELECT ON performance_schema.* TO 'datadog'@'%';
9191
Query OK, 0 rows affected (0.00 sec)
9292
```
9393

94+
To collect index metrics, grant the `datadog` user an additional privilege:
95+
96+
```shell
97+
98+
mysql> "GRANT SELECT ON mysql.innodb_index_stats TO 'datadog'@'%'";
99+
Query OK, 0 rows affected (0.00 sec)
100+
```
101+
94102
### Configuration
95103

96104
Follow the instructions below to configure this check for an Agent running on a host. For containerized environments, see the [Docker](?tab=docker#docker), [Kubernetes](?tab=kubernetes#kubernetes), or [ECS](?tab=ecs#ecs) sections.

mysql/assets/configuration/spec.yaml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,6 @@ files:
336336
value:
337337
type: boolean
338338
example: false
339-
340339
- name: extra_performance_metrics
341340
description: |
342341
These metrics are reported if `performance_schema` is enabled in the MySQL instance
@@ -543,6 +542,31 @@ files:
543542
value:
544543
type: number
545544
example: 10
545+
- name: index_metrics
546+
description: |
547+
Configure collection of index metrics.
548+
Metrics provided by the options:
549+
- mysql.index.size (per index)
550+
- mysql.index.reads (per index)
551+
- mysql.index.updates (per index)
552+
- mysql.index.deletes (per index)
553+
Note that the index size metric requires the user defined for this instance to have
554+
SELECT privileges. Take a look at
555+
https://docs.datadoghq.com/integrations/mysql/?tab=host#prepare-mysql for further instructions.
556+
options:
557+
- name: enabled
558+
description: |
559+
Enable collection of index metrics.
560+
value:
561+
type: boolean
562+
example: true
563+
- name: collection_interval
564+
hidden: true
565+
description: |
566+
Set the index metrics collection interval (in seconds). Defaults to 5 minutes.
567+
value:
568+
type: number
569+
example: 300
546570
- name: aws
547571
description: |
548572
This block defines the configuration for AWS RDS and Aurora instances.

mysql/changelog.d/19383.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Emit index usage and index metrics from mysql integration

mysql/datadog_checks/mysql/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def __init__(self, instance, init_config):
4747
self.settings_config = instance.get('collect_settings', {}) or {}
4848
self.activity_config = instance.get('query_activity', {}) or {}
4949
self.schemas_config: dict = instance.get('schemas_collection', {}) or {}
50+
self.index_config: dict = instance.get('index_metrics', {}) or {}
5051

5152
self.cloud_metadata = {}
5253
aws = instance.get('aws', {})

mysql/datadog_checks/mysql/config_models/instance.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,15 @@ class Gcp(BaseModel):
6767
project_id: Optional[str] = None
6868

6969

70+
class IndexMetrics(BaseModel):
71+
model_config = ConfigDict(
72+
arbitrary_types_allowed=True,
73+
frozen=True,
74+
)
75+
collection_interval: Optional[float] = None
76+
enabled: Optional[bool] = None
77+
78+
7079
class MetricPatterns(BaseModel):
7180
model_config = ConfigDict(
7281
arbitrary_types_allowed=True,
@@ -196,6 +205,7 @@ class InstanceConfig(BaseModel):
196205
empty_default_hostname: Optional[bool] = None
197206
gcp: Optional[Gcp] = None
198207
host: Optional[str] = None
208+
index_metrics: Optional[IndexMetrics] = None
199209
log_unobfuscated_plans: Optional[bool] = None
200210
log_unobfuscated_queries: Optional[bool] = None
201211
max_custom_queries: Optional[int] = None

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,23 @@ instances:
499499
#
500500
# collection_interval: 10
501501

502+
## Configure collection of index metrics.
503+
## Metrics provided by the options:
504+
## - mysql.index.size (per index)
505+
## - mysql.index.reads (per index)
506+
## - mysql.index.updates (per index)
507+
## - mysql.index.deletes (per index)
508+
## Note that the index size metric requires the user defined for this instance to have
509+
## SELECT privileges. Take a look at
510+
## https://docs.datadoghq.com/integrations/mysql/?tab=host#prepare-mysql for further instructions.
511+
#
512+
# index_metrics:
513+
514+
## @param enabled - boolean - optional - default: true
515+
## Enable collection of index metrics.
516+
#
517+
# enabled: true
518+
502519
## This block defines the configuration for AWS RDS and Aurora instances.
503520
##
504521
## Complete this section if you have installed the Datadog AWS Integration
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# (C) Datadog, Inc. 2025-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
5+
from datadog_checks.base import is_affirmative
6+
7+
QUERY_INDEX_SIZE = {
8+
'name': 'mysql.innodb_index_stats',
9+
'query': """
10+
SELECT
11+
database_name,
12+
table_name,
13+
index_name,
14+
stat_value * @@innodb_page_size AS index_size_bytes
15+
FROM
16+
mysql.innodb_index_stats
17+
WHERE
18+
stat_name = 'size'
19+
""".strip(),
20+
'columns': [
21+
{'name': 'db', 'type': 'tag'},
22+
{'name': 'table', 'type': 'tag'},
23+
{'name': 'index', 'type': 'tag'},
24+
{'name': 'mysql.index.size', 'type': 'gauge'},
25+
],
26+
}
27+
QUERY_INDEX_USAGE = {
28+
'name': 'performance_schema.table_io_waits_summary_by_index_usage',
29+
'query': """
30+
SELECT
31+
object_schema,
32+
object_name,
33+
index_name,
34+
count_read,
35+
count_update,
36+
count_delete
37+
FROM
38+
performance_schema.table_io_waits_summary_by_index_usage
39+
WHERE index_name IS NOT NULL
40+
AND object_schema NOT IN ('mysql', 'performance_schema')
41+
""".strip(),
42+
'columns': [
43+
{'name': 'db', 'type': 'tag'},
44+
{'name': 'table', 'type': 'tag'},
45+
{'name': 'index', 'type': 'tag'},
46+
{'name': 'mysql.index.reads', 'type': 'gauge'},
47+
{'name': 'mysql.index.updates', 'type': 'gauge'},
48+
{'name': 'mysql.index.deletes', 'type': 'gauge'},
49+
],
50+
}
51+
52+
DEFAULT_INDEX_METRIC_COLLECTION_INTERVAL = 300 # 5 minutes
53+
54+
55+
class MySqlIndexMetrics:
56+
def __init__(self, config):
57+
self._config = config
58+
59+
@property
60+
def include_index_metrics(self) -> bool:
61+
return is_affirmative(self._config.index_config.get('enabled', True))
62+
63+
@property
64+
def collection_interval(self) -> int:
65+
return int(self._config.index_config.get('collection_interval', DEFAULT_INDEX_METRIC_COLLECTION_INTERVAL))
66+
67+
@property
68+
def queries(self):
69+
# make a copy of the query to avoid modifying the original
70+
# in case different instances have different collection intervals
71+
usage_query = QUERY_INDEX_USAGE.copy()
72+
size_query = QUERY_INDEX_SIZE.copy()
73+
usage_query['collection_interval'] = self.collection_interval
74+
size_query['collection_interval'] = self.collection_interval
75+
return [size_query, usage_query]

mysql/datadog_checks/mysql/mysql.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
TABLE_VARS,
5555
VARIABLES_VARS,
5656
)
57+
from .index_metrics import MySqlIndexMetrics
5758
from .innodb_metrics import InnoDBMetrics
5859
from .metadata import MySQLMetadata
5960
from .queries import (
@@ -130,6 +131,7 @@ def __init__(self, name, init_config, instances):
130131
self._statement_samples = MySQLStatementSamples(self, self._config, self._get_connection_args())
131132
self._mysql_metadata = MySQLMetadata(self, self._config, self._get_connection_args())
132133
self._query_activity = MySQLActivity(self, self._config, self._get_connection_args())
134+
self._index_metrics = MySqlIndexMetrics(self._config)
133135
# _database_instance_emitted: limit the collection and transmission of the database instance metadata
134136
self._database_instance_emitted = TTLCache(
135137
maxsize=1,
@@ -378,7 +380,8 @@ def _get_runtime_queries(self, db):
378380

379381
if self.performance_schema_enabled:
380382
queries.extend([QUERY_USER_CONNECTIONS])
381-
383+
if self._index_metrics.include_index_metrics:
384+
queries.extend(self._index_metrics.queries)
382385
self._runtime_queries_cached = self._new_query_executor(queries)
383386
self._runtime_queries_cached.compile_queries()
384387
self.log.debug("initialized runtime queries")

mysql/metadata.csv

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ mysql.galera.wsrep_local_state,gauge,,,,Internal Galera cluster state number,0,m
1717
mysql.galera.wsrep_received,gauge,,,,Total number of write-sets received from other nodes.,0,mysql,mysql galera wsrep_received,
1818
mysql.galera.wsrep_received_bytes,gauge,,,,Total size (in bytes) of writesets received from other nodes.,0,mysql,mysql galera wsrep_received_bytes,
1919
mysql.galera.wsrep_replicated_bytes,gauge,,,,Total size (in bytes) of writesets sent to other nodes.,0,mysql,mysql galera wsrep_replicated_bytes,
20+
mysql.index.deletes,gauge,,operation,,The number of delete operations using an index. Resets to 0 upon database restart.,0,mysql,mysql index delete usage,
21+
mysql.index.reads,gauge,,operation,,The number of read operations using an index. Resets to 0 upon database restart.,0,mysql,mysql index read usage,
22+
mysql.index.size,gauge,,byte,,Size of index in bytes,0,mysql,mysql index size,
23+
mysql.index.updates,gauge,,operation,,The number of update operations using an index. Resets to 0 upon database restart.,0,mysql,mysql index update usage,
2024
mysql.info.schema.size,gauge,,mebibyte,,Size of schemas in MiB,0,mysql,mysql schema size,
2125
mysql.info.table.data_size,gauge,,mebibyte,,Size of tables data in MiB,0,mysql,mysql data table size,memory
2226
mysql.info.table.index_size,gauge,,mebibyte,,Size of tables index in MiB,0,mysql,mysql index table size,

mysql/tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def instance_complex():
102102
'table_size_metrics': True,
103103
'system_table_size_metrics': True,
104104
'table_row_stats_metrics': True,
105+
'index_metrics': True,
105106
},
106107
'tags': tags.METRIC_TAGS,
107108
'queries': [
@@ -405,6 +406,7 @@ def _add_dog_user(conn):
405406
cur.execute("GRANT PROCESS ON *.* TO 'dog'@'%'")
406407
cur.execute("GRANT REPLICATION CLIENT ON *.* TO 'dog'@'%'")
407408
cur.execute("GRANT SELECT ON performance_schema.* TO 'dog'@'%'")
409+
cur.execute("GRANT SELECT ON mysql.innodb_index_stats TO 'dog'@'%'")
408410

409411
# refactor try older mysql.user table first. if this fails, go to newer ALTER USER
410412
try:

mysql/tests/test_mysql.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ def test_minimal_config(aggregator, dd_run_check, instance_basic):
4848
+ variables.INNODB_VARS
4949
+ variables.BINLOG_VARS
5050
+ variables.COMMON_PERFORMANCE_VARS
51+
+ variables.INDEX_SIZE_VARS
52+
+ variables.INDEX_USAGE_VARS
5153
)
5254

5355
operation_time_metrics = (
@@ -73,7 +75,6 @@ def test_minimal_config(aggregator, dd_run_check, instance_basic):
7375
continue
7476
else:
7577
aggregator.assert_metric(mname, at_least=1)
76-
aggregator.assert_metric(mname, at_least=1)
7778

7879
optional_metrics = (
7980
variables.COMPLEX_STATUS_VARS
@@ -161,6 +162,8 @@ def _assert_complex_config(aggregator, service_check_tags, metric_tags, hostname
161162
+ variables.STATEMENT_VARS
162163
+ variables.TABLE_VARS
163164
+ variables.ROW_TABLE_STATS_VARS
165+
+ variables.INDEX_SIZE_VARS
166+
+ variables.INDEX_USAGE_VARS
164167
)
165168

166169
operation_time_metrics = variables.SIMPLE_OPERATION_TIME_METRICS + variables.COMPLEX_OPERATION_TIME_METRICS
@@ -233,6 +236,7 @@ def _assert_complex_config(aggregator, service_check_tags, metric_tags, hostname
233236
aggregator.assert_metric('alice.age', value=25)
234237
aggregator.assert_metric('bob.age', value=20)
235238

239+
_test_index_metrics(aggregator, variables.INDEX_USAGE_VARS + variables.INDEX_SIZE_VARS, metric_tags)
236240
# test optional metrics
237241
optional_metrics = (
238242
variables.OPTIONAL_REPLICATION_METRICS
@@ -304,6 +308,7 @@ def test_complex_config_replica(aggregator, dd_run_check, instance_complex):
304308
+ variables.STATEMENT_VARS
305309
+ variables.TABLE_VARS
306310
+ variables.ROW_TABLE_STATS_VARS
311+
+ variables.INDEX_SIZE_VARS
307312
)
308313

309314
operation_time_metrics = (
@@ -313,7 +318,9 @@ def test_complex_config_replica(aggregator, dd_run_check, instance_complex):
313318
)
314319

315320
if MYSQL_VERSION_PARSED >= parse_version('5.6') and MYSQL_FLAVOR != 'mariadb':
316-
testable_metrics.extend(variables.PERFORMANCE_VARS + variables.COMMON_PERFORMANCE_VARS)
321+
testable_metrics.extend(
322+
variables.PERFORMANCE_VARS + variables.COMMON_PERFORMANCE_VARS + variables.INDEX_USAGE_VARS
323+
)
317324
operation_time_metrics.extend(
318325
variables.COMMON_PERFORMANCE_OPERATION_TIME_METRICS + variables.PERFORMANCE_OPERATION_TIME_METRICS
319326
)
@@ -458,6 +465,43 @@ def test_correct_hostname(dbm_enabled, reported_hostname, expected_hostname, agg
458465
aggregator.assert_metric(mname, hostname=expected_hostname, at_least=0)
459466

460467

468+
def _test_index_metrics(aggregator, index_metrics, metric_tags):
469+
for mname in index_metrics:
470+
if mname in ['mysql.index.reads', 'mysql.index.updates', 'mysql.index.deletes']:
471+
aggregator.assert_metric(
472+
mname,
473+
tags=metric_tags + ['db:testdb', 'table:users', 'index:id'],
474+
count=1,
475+
)
476+
aggregator.assert_metric(
477+
mname,
478+
tags=metric_tags
479+
+ [
480+
'db:datadog_test_schemas',
481+
'table:cities',
482+
'index:single_column_index',
483+
],
484+
count=1,
485+
)
486+
aggregator.assert_metric(
487+
mname,
488+
tags=metric_tags + ['db:datadog_test_schemas', 'table:cities', 'index:two_columns_index'],
489+
count=1,
490+
)
491+
if mname == 'mysql.index.size':
492+
aggregator.assert_metric(mname, tags=metric_tags + ['db:testdb', 'table:users', 'index:id'], count=1)
493+
aggregator.assert_metric(
494+
mname,
495+
tags=metric_tags + ['db:datadog_test_schemas', 'table:cities', 'index:single_column_index'],
496+
count=1,
497+
)
498+
aggregator.assert_metric(
499+
mname,
500+
tags=metric_tags + ['db:datadog_test_schemas', 'table:cities', 'index:two_columns_index'],
501+
count=1,
502+
)
503+
504+
461505
def _test_optional_metrics(aggregator, optional_metrics):
462506
"""
463507
Check optional metrics - They can either be present or not

mysql/tests/variables.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,15 @@
280280
'mysql.replication.group.transactions_rollback',
281281
]
282282

283+
INDEX_SIZE_VARS = [
284+
'mysql.index.size',
285+
]
286+
INDEX_USAGE_VARS = [
287+
'mysql.index.reads',
288+
'mysql.index.updates',
289+
'mysql.index.deletes',
290+
]
291+
283292
SIMPLE_OPERATION_TIME_METRICS = [
284293
'status_metrics',
285294
'innodb_metrics',

0 commit comments

Comments
 (0)