Skip to content

Commit eb32def

Browse files
authored
Add filtering dbs by regex capability in pgbouncer integration (DataDog#23111)
* Add filtering dbs by regex capability in pgbouncer integration * PGBouncer - Regenerated conf.yaml.example * PGBouncer - filter by regex, fix default value * PGBouncer - Make new attribute field_configurable like the other attributes * Pgbouncer - Added changelog entry
1 parent be9e138 commit eb32def

6 files changed

Lines changed: 86 additions & 0 deletions

File tree

pgbouncer/assets/configuration/spec.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ files:
1818
value:
1919
example: postgresql://<USERNAME>:<PASSWORD>@<HOSTNAME>:<PORT>/<DATABASE_URL>?sslmode=require
2020
type: string
21+
- name: database_filter_regex
22+
description: |
23+
Collect metrics only for PgBouncer database names matching this regular expression.
24+
25+
The regex is applied to the PgBouncer database name exposed by the admin views.
26+
Global metrics that are not tied to a specific database, such as configuration metrics,
27+
are still collected.
28+
value:
29+
example: ^(datadog_test|dogs)$
30+
type: string
31+
display_default: null
32+
fleet_configurable: true
2133
- name: host
2234
description: If `database_url` is not used, set up the host to connect to with the `host` parameter.
2335
value:

pgbouncer/changelog.d/23111.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add filtering databases by regex capability.

pgbouncer/datadog_checks/pgbouncer/config_models/instance.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class InstanceConfig(BaseModel):
3636
)
3737
collect_per_client_metrics: Optional[bool] = None
3838
collect_per_server_metrics: Optional[bool] = None
39+
database_filter_regex: Optional[str] = None
3940
database_url: Optional[str] = None
4041
disable_generic_tags: Optional[bool] = None
4142
empty_default_hostname: Optional[bool] = None

pgbouncer/datadog_checks/pgbouncer/data/conf.yaml.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ instances:
2323
#
2424
# database_url: postgresql://<USERNAME>:<PASSWORD>@<HOSTNAME>:<PORT>/<DATABASE_URL>?sslmode=require
2525

26+
## @param database_filter_regex - string - optional
27+
## Collect metrics only for PgBouncer database names matching this regular expression.
28+
##
29+
## The regex is applied to the PgBouncer database name exposed by the admin views.
30+
## Global metrics that are not tied to a specific database, such as configuration metrics,
31+
## are still collected.
32+
#
33+
# database_filter_regex: ^(datadog_test|dogs)$
34+
2635
## @param host - string - optional
2736
## If `database_url` is not used, set up the host to connect to with the `host` parameter.
2837
#

pgbouncer/datadog_checks/pgbouncer/pgbouncer.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,17 @@ def __init__(self, name, init_config, instances):
3838
self.password = self.instance.get('password', '')
3939
self.tags = self.instance.get('tags', [])
4040
self.database_url = self.instance.get('database_url')
41+
self.database_filter_regex = self.instance.get('database_filter_regex', '')
4142
self.use_cached = is_affirmative(self.instance.get('use_cached', True))
4243
self.collect_per_client_metrics = is_affirmative(self.instance.get('collect_per_client_metrics', False))
4344
self.collect_per_server_metrics = is_affirmative(self.instance.get('collect_per_server_metrics', False))
45+
self.database_filter = None
46+
47+
if self.database_filter_regex:
48+
try:
49+
self.database_filter = re.compile(self.database_filter_regex)
50+
except re.error as e:
51+
raise ConfigurationError('Invalid database_filter_regex: {}'.format(e))
4452

4553
if not self.database_url:
4654
if not self.host:
@@ -100,6 +108,9 @@ def _collect_stats(self, db):
100108
elif row.get('database') == self.DB_NAME:
101109
continue
102110

111+
if not self._should_collect_row(row):
112+
continue
113+
103114
tags = list(self.tags)
104115
tags += ["%s:%s" % (tag, row[column]) for (column, tag) in descriptors if column in row]
105116
for column, (name, reporter) in metrics:
@@ -136,6 +147,23 @@ def iter_rows(self, cursor):
136147

137148
row_num += 1
138149

150+
def _get_row_database_name(self, row):
151+
if 'name' in row:
152+
return row['name']
153+
if 'database' in row:
154+
return row['database']
155+
return None
156+
157+
def _should_collect_row(self, row):
158+
if not self.database_filter:
159+
return True
160+
161+
database_name = self._get_row_database_name(row)
162+
if database_name is None:
163+
return True
164+
165+
return self.database_filter.search(database_name) is not None
166+
139167
def _get_connect_kwargs(self):
140168
"""
141169
Get the params to pass to psycopg.connect() based on passed-in vals

pgbouncer/tests/test_pgbouncer_unit.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,41 @@ def test_config_missing_user(instance):
2727
PgBouncer('pgbouncer', {}, [instance])
2828

2929

30+
@pytest.mark.unit
31+
def test_config_invalid_database_filter_regex(instance):
32+
instance['database_filter_regex'] = '('
33+
34+
with pytest.raises(ConfigurationError, match='Invalid database_filter_regex'):
35+
PgBouncer('pgbouncer', {}, [instance])
36+
37+
38+
@pytest.mark.unit
39+
def test_database_filter_matches_database_rows(instance):
40+
instance['database_filter_regex'] = '^dogs$'
41+
check = PgBouncer('pgbouncer', {}, [instance])
42+
43+
assert check._should_collect_row({'database': 'dogs'})
44+
assert not check._should_collect_row({'database': 'datadog_test'})
45+
46+
47+
@pytest.mark.unit
48+
def test_database_filter_matches_show_databases_rows(instance):
49+
instance['database_filter_regex'] = '^dogs$'
50+
check = PgBouncer('pgbouncer', {}, [instance])
51+
52+
assert check._get_row_database_name({'name': 'dogs', 'database': 'postgres_dogs'}) == 'dogs'
53+
assert check._should_collect_row({'name': 'dogs'})
54+
assert not check._should_collect_row({'name': 'datadog_test'})
55+
56+
57+
@pytest.mark.unit
58+
def test_database_filter_keeps_global_rows(instance):
59+
instance['database_filter_regex'] = '^dogs$'
60+
check = PgBouncer('pgbouncer', {}, [instance])
61+
62+
assert check._should_collect_row({'key': 'max_client_conn', 'value': '100'})
63+
64+
3065
@pytest.mark.unit
3166
@pytest.mark.parametrize('use_cached', [True, False])
3267
def test_connection_cleanup_on_error(instance, use_cached):

0 commit comments

Comments
 (0)