Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade PGBouncer to psycopg3 #19325

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .builders/images/macos-x86_64/builder_setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ rm "${DD_PREFIX_PATH}/bin/curl"

# libpq and pg_config as needed by psycopg
DOWNLOAD_URL="https://ftp.postgresql.org/pub/source/v{{version}}/postgresql-{{version}}.tar.bz2" \
VERSION="16.0" \
SHA256="df9e823eb22330444e1d48e52cc65135a652a6fdb3ce325e3f08549339f51b99" \
VERSION="16.6" \
SHA256="23369cdaccd45270ac5dcc30fa9da205d5be33fa505e1f17a0418d2caeca477b" \
RELATIVE_PATH="postgresql-{{version}}" \
install-from-source --without-readline --with-openssl --without-icu

Expand Down
6 changes: 6 additions & 0 deletions .ddev/ci/scripts/pgbouncer/linux/50_install_postgres.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash

set -ex

sudo apt update
sudo apt install -y --no-install-recommends build-essential python3-dev libpq-dev
7 changes: 0 additions & 7 deletions .ddev/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,6 @@ mmh3 = ['CC0-1.0']
paramiko = ['LGPL-2.1-only']
# https://github.com/psycopg/psycopg/blob/master/LICENSE.txt
psycopg = ['LGPL-3.0-only']
# https://github.com/psycopg/psycopg2/blob/master/LICENSE
# https://github.com/psycopg/psycopg2/blob/master/doc/COPYING.LESSER
psycopg2-binary = ['LGPL-3.0-only', 'BSD-3-Clause']
# https://github.com/Legrandin/pycryptodome/blob/master/LICENSE.rst
pycryptodomex = ['Unlicense', 'BSD-2-Clause']
# https://github.com/mongodb/mongo-python-driver/blob/master/LICENSE
Expand Down Expand Up @@ -118,7 +115,6 @@ lxml = 'https://github.com/lxml/lxml'
packaging = 'https://github.com/pypa/packaging'
paramiko = 'https://github.com/paramiko/paramiko'
protobuf = 'https://github.com/protocolbuffers/protobuf'
psycopg2-binary = 'https://github.com/psycopg/psycopg2'
pycryptodomex = 'https://github.com/Legrandin/pycryptodome'
redis = 'https://github.com/redis/redis-py'
requests = 'https://github.com/psf/requests'
Expand Down Expand Up @@ -170,9 +166,6 @@ exclude = [
'aerospike', # v8+ breaks agent build.
# https://github.com/DataDog/integrations-core/pull/16080
'lxml',
# We're not ready to switch to v3 of the postgres library, see:
# https://github.com/DataDog/integrations-core/pull/15859
'psycopg2-binary',
'psutil',
'pymongo[srv]', # Upgrade from 4.8.0 to 4.10.1 causes "AttributeError: module 'pymongo' has no attribute 'mongo_client'"
]
Expand Down
2 changes: 0 additions & 2 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ prometheus-client,PyPI,Apache-2.0,Copyright 2015 The Prometheus Authors
protobuf,PyPI,BSD-3-Clause,Copyright 2008 Google Inc. All rights reserved.
psutil,PyPI,BSD-3-Clause,"Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola"
psycopg,PyPI,LGPL-3.0-only,Copyright (C) 2020 The Psycopg Team
psycopg2-binary,PyPI,BSD-3-Clause,Copyright 2013 Federico Di Gregorio
psycopg2-binary,PyPI,LGPL-3.0-only,Copyright (C) 2013 Federico Di Gregorio
pyOpenSSL,PyPI,Apache-2.0,Copyright The pyOpenSSL developers
pyasn1,PyPI,BSD-3-Clause,"Copyright (c) 2005-2019, Ilya Etingof <[email protected]>"
pycryptodomex,PyPI,BSD-2-Clause,Copyright 2014 Helder Eijs
Expand Down
1 change: 0 additions & 1 deletion agent_requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ ply==3.11
prometheus-client==0.21.1
protobuf==5.29.3
psutil==6.0.0
psycopg2-binary==2.9.9
psycopg[c]==3.2.3
pyasn1==0.4.8
pycryptodomex==3.21.0
Expand Down
1 change: 1 addition & 0 deletions datadog_checks_dev/changelog.d/19325.security
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Removed dependency on psycopg2
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@
'mmh3': ['CC0-1.0'],
# https://github.com/paramiko/paramiko/blob/master/LICENSE
'paramiko': ['LGPL-2.1-only'],
# https://github.com/psycopg/psycopg2/blob/master/LICENSE
# https://github.com/psycopg/psycopg2/blob/master/doc/COPYING.LESSER
'psycopg2-binary': ['LGPL-3.0-only', 'BSD-3-Clause'],
# https://github.com/psycopg/psycopg/blob/master/LICENSE.txt
'psycopg': ['LGPL-3.0-only'],
# https://github.com/psycopg/psycopg/blob/master/psycopg_pool/LICENSE.txt
Expand Down Expand Up @@ -161,7 +158,6 @@
'packaging': 'https://github.com/pypa/packaging',
'paramiko': 'https://github.com/paramiko/paramiko',
'protobuf': 'https://github.com/protocolbuffers/protobuf',
'psycopg2-binary': 'https://github.com/psycopg/psycopg2',
'psycopg': 'https://github.com/psycopg/psycopg',
'pycryptodomex': 'https://github.com/Legrandin/pycryptodome',
'redis': 'https://github.com/redis/redis-py',
Expand Down
1 change: 1 addition & 0 deletions pgbouncer/changelog.d/19325.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Upgrade pgbouncer to psycopg3
1 change: 1 addition & 0 deletions pgbouncer/changelog.d/19325.security
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Removed dependency on psycopg2
122 changes: 62 additions & 60 deletions pgbouncer/datadog_checks/pgbouncer/pgbouncer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import time
from urllib.parse import urlparse

import psycopg2 as pg
from psycopg2 import extras as pgextras
import psycopg as pg
from psycopg import ClientCursor
from psycopg.rows import dict_row

from datadog_checks.base import AgentCheck, ConfigurationError, is_affirmative
from datadog_checks.pgbouncer.metrics import (
Expand Down Expand Up @@ -73,47 +74,46 @@ def _collect_stats(self, db):
metric_scope.append(SERVERS_METRICS)

try:
with db.cursor(cursor_factory=pgextras.DictCursor) as cursor:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PG3 with automatically starts a transaction, i.e., adds a BEGIN, which breaks many of the queries.

for scope in metric_scope:
descriptors = scope['descriptors']
metrics = scope['metrics']
query = scope['query']

try:
self.log.debug("Running query: %s", query)
cursor.execute(query)
rows = self.iter_rows(cursor)

except Exception as e:
self.log.exception("Not all metrics may be available: %s", str(e))

else:
for row in rows:
if 'key' in row: # We are processing "config metrics"
# Make a copy of the row to allow mutation
# (a `psycopg2.lib.extras.DictRow` object doesn't accept a new key)
row = row.copy()
# We flip/rotate the row: row value becomes the column name
row[row['key']] = row['value']
# Skip the "pgbouncer" database
elif row.get('database') == self.DB_NAME:
continue

tags = list(self.tags)
tags += ["%s:%s" % (tag, row[column]) for (column, tag) in descriptors if column in row]
for column, (name, reporter) in metrics:
if column in row:
value = row[column]
if column in ['connect_time', 'request_time']:
self.log.debug("Parsing timestamp; original value: %s", value)
# First get rid of any UTC suffix.
value = re.findall(r'^[^ ]+ [^ ]+', value)[0]
value = time.strptime(value, '%Y-%m-%d %H:%M:%S')
value = time.mktime(value)
reporter(self, name, value, tags)

if not rows:
self.log.warning("No results were found for query: %s", query)
for scope in metric_scope:
descriptors = scope['descriptors']
metrics = scope['metrics']
query = scope['query']

try:
cursor = db.cursor(row_factory=dict_row)
self.log.debug("Running query: %s", query)
cursor.execute(query)
rows = self.iter_rows(cursor)

except Exception as e:
self.log.exception("Not all metrics may be available: %s", str(e))

else:
for row in rows:
if 'key' in row: # We are processing "config metrics"
# Make a copy of the row to allow mutation
row = row.copy()
# We flip/rotate the row: row value becomes the column name
row[row['key']] = row['value']
# Skip the "pgbouncer" database
elif row.get('database') == self.DB_NAME:
continue

tags = list(self.tags)
tags += ["%s:%s" % (tag, row[column]) for (column, tag) in descriptors if column in row]
for column, (name, reporter) in metrics:
if column in row:
value = row[column]
if column in ['connect_time', 'request_time']:
self.log.debug("Parsing timestamp; original value: %s", value)
# First get rid of any UTC suffix.
value = re.findall(r'^[^ ]+ [^ ]+', value)[0]
value = time.strptime(value, '%Y-%m-%d %H:%M:%S')
value = time.mktime(value)
reporter(self, name, value, tags)

if not rows:
self.log.warning("No results were found for query: %s", query)

except pg.Error:
self.log.exception("Connection error")
Expand All @@ -138,21 +138,25 @@ def iter_rows(self, cursor):

def _get_connect_kwargs(self):
"""
Get the params to pass to psycopg2.connect() based on passed-in vals
Get the params to pass to psycopg.connect() based on passed-in vals
from yaml settings file
"""
# It's important to set the client_encoding to utf-8
# PGBouncer defaults to an encoding of 'UNICODE`, which will cause psycopg to error out
if self.database_url:
return {'dsn': self.database_url}
return {'conninfo': self.database_url, 'client_encoding': 'utf-8'}

if self.host in ('localhost', '127.0.0.1') and self.password == '':
# Use ident method
return {'dsn': "user={} dbname={}".format(self.user, self.DB_NAME)}
return {'conninfo': "user={} dbname={} client_encoding=utf-8".format(self.user, self.DB_NAME)}

args = {
'host': self.host,
'user': self.user,
'password': self.password,
'database': self.DB_NAME,
'dbname': self.DB_NAME,
'cursor_factory': ClientCursor,
'client_encoding': 'utf-8',
}
if self.port:
args['port'] = self.port
Expand All @@ -166,8 +170,9 @@ def _get_connection(self, use_cached=None):
return self.connection
try:
connect_kwargs = self._get_connect_kwargs()
connection = pg.connect(**connect_kwargs)
connection.set_isolation_level(pg.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
# Somewhat counterintuitively, we need to set autocommit to True to avoid a BEGIN/COMMIT block
# https://www.psycopg.org/psycopg3/docs/basic/transactions.html#autocommit-transactions
connection = pg.connect(**connect_kwargs, autocommit=True)
except Exception:
redacted_url = self._get_redacted_dsn()
message = u'Cannot establish connection to {}'.format(redacted_url)
Expand All @@ -180,6 +185,10 @@ def _get_connection(self, use_cached=None):
self.connection = connection
return connection

def _close_connection(self):
self.connection.close()
self.connection = None

def _get_redacted_dsn(self):
if not self.database_url:
return u'pgbouncer://%s:******@%s:%s/%s' % (self.user, self.host, self.port, self.DB_NAME)
Expand All @@ -200,6 +209,8 @@ def check(self, instance):

self.service_check(self.SERVICE_CHECK_NAME, AgentCheck.OK, tags=self._get_service_checks_tags())
self._set_metadata()
# Avoid holding an open connection
self._close_connection()

def _set_metadata(self):
if self.is_metadata_collection_enabled():
Expand All @@ -209,14 +220,5 @@ def _set_metadata(self):

def get_version(self):
db = self._get_connection()
regex = r'\d+\.\d+\.\d+'
with db.cursor(cursor_factory=pgextras.DictCursor) as cursor:
cursor.execute('SHOW VERSION;')
if db.notices:
data = db.notices[0]
else:
data = cursor.fetchone()[0]
res = re.findall(regex, data)
if res:
return res[0]
self.log.debug("Couldn't detect version from %s", data)
version = pg.pq.version_pretty(db.connection.info.server_version)
return version
2 changes: 1 addition & 1 deletion pgbouncer/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ license = "BSD-3-Clause"

[project.optional-dependencies]
deps = [
"psycopg2-binary==2.9.9",
"psycopg[c]==3.2.3",
]

[project.urls]
Expand Down
7 changes: 7 additions & 0 deletions pgbouncer/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
'tags': ['optional:tag1'],
}

E2E_METADATA = {
'start_commands': [
'apt update',
'apt install -y --no-install-recommends build-essential python3-dev libpq-dev',
],
}


def get_version_from_env():
return version.parse(os.environ.get('PGBOUNCER_VERSION'))
8 changes: 4 additions & 4 deletions pgbouncer/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import os
from copy import deepcopy

import psycopg2
import psycopg
import pytest
from packaging import version

Expand All @@ -20,8 +20,8 @@ def container_up(service_name, port):
"""
Try to connect to postgres/pgbouncer
"""
psycopg2.connect(
host=common.HOST, port=port, user=common.USER, password=common.PASS, database=common.DB, connect_timeout=2
psycopg.connect(
host=common.HOST, port=port, user=common.USER, password=common.PASS, dbname=common.DB, connect_timeout=2
)


Expand All @@ -45,7 +45,7 @@ def dd_environment():
],
):

yield common.DEFAULT_INSTANCE
yield common.DEFAULT_INSTANCE, common.E2E_METADATA


@pytest.fixture
Expand Down
17 changes: 7 additions & 10 deletions pgbouncer/tests/test_pgbouncer_integration_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# All rights reserved
# Licensed under Simplified BSD License (see LICENSE)

import psycopg2
import psycopg
import pytest
from packaging import version

Expand All @@ -15,15 +15,14 @@
@pytest.mark.usefixtures("dd_environment")
def test_check(instance, aggregator, datadog_agent, dd_run_check):
# add some stats
connection = psycopg2.connect(
connection = psycopg.connect(
host=common.HOST,
port=common.PORT,
user=common.USER,
password=common.PASS,
database=common.DB,
dbname=common.DB,
connect_timeout=1,
)
connection.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cur = connection.cursor()
cur.execute('SELECT * FROM persons;')

Expand All @@ -49,15 +48,14 @@ def test_check(instance, aggregator, datadog_agent, dd_run_check):
@pytest.mark.usefixtures("dd_environment")
def test_check_with_clients(instance, aggregator, datadog_agent, dd_run_check):
# add some stats
connection = psycopg2.connect(
connection = psycopg.connect(
host=common.HOST,
port=common.PORT,
user=common.USER,
password=common.PASS,
database=common.DB,
dbname=common.DB,
connect_timeout=1,
)
connection.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cur = connection.cursor()
cur.execute('SELECT * FROM persons;')

Expand All @@ -80,15 +78,14 @@ def test_check_with_clients(instance, aggregator, datadog_agent, dd_run_check):
@pytest.mark.usefixtures("dd_environment")
def test_check_with_servers(instance, aggregator, datadog_agent, dd_run_check):
# add some stats
connection = psycopg2.connect(
connection = psycopg.connect(
host=common.HOST,
port=common.PORT,
user=common.USER,
password=common.PASS,
database=common.DB,
dbname=common.DB,
connect_timeout=1,
)
connection.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cur = connection.cursor()
cur.execute('SELECT * FROM persons;')

Expand Down
Loading