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

Extract version utils and use semver for version comparison #4844

Merged
merged 39 commits into from
Oct 31, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
42ccb88
Remove multi instance from code
hithwen Oct 18, 2019
ffe81db
Fix some tests
hithwen Oct 18, 2019
294602f
Remove known hosts
hithwen Oct 21, 2019
4e3c315
Do not modify constants
hithwen Oct 21, 2019
755c960
Do not iterate on instances on constructor
hithwen Oct 21, 2019
dd10d0e
DB is always cached, do not pass around
hithwen Oct 21, 2019
c600eb6
Extract version utils
hithwen Oct 21, 2019
9c7ce54
Fix unit tests
hithwen Oct 21, 2019
fb9784a
Add missing argument
hithwen Oct 21, 2019
962a8fa
Merge branch 'julia/postgres-refactor' into julia/pg-refactor-cached-…
hithwen Oct 21, 2019
ab8d4ea
Merge branch 'julia/pg-refactor-cached-connection' into julia/pg-refa…
hithwen Oct 21, 2019
5928819
Fix unit tests
hithwen Oct 21, 2019
f4022d3
Merge branch 'julia/postgres-refactor' into julia/pg-refactor-cached-…
hithwen Oct 21, 2019
7b0a249
Merge branch 'julia/pg-refactor-cached-connection' into julia/pg-refa…
hithwen Oct 21, 2019
81f9d5f
Remove unused argument
hithwen Oct 22, 2019
b7c9f74
Merge remote-tracking branch 'origin/master' into julia/pg-refactor-c…
hithwen Oct 23, 2019
0fe217d
Style fix
hithwen Oct 23, 2019
5398b24
Merge branch 'julia/pg-refactor-cached-connection' into julia/pg-refa…
hithwen Oct 23, 2019
768a356
Add semver dependency
hithwen Oct 23, 2019
d12d774
Add semver dependency to agent_requirements, create version constants
hithwen Oct 23, 2019
a0401c6
Style fix
hithwen Oct 23, 2019
6b3af0d
Fix tests
hithwen Oct 23, 2019
1cb0e14
Merge remote-tracking branch 'origin/master' into julia/pg-refactor-u…
hithwen Oct 24, 2019
c35f28a
fix metadata
hithwen Oct 24, 2019
0ddbb8a
Add missing import
hithwen Oct 24, 2019
e472798
Fix tests
hithwen Oct 24, 2019
6ab4df9
Remove unneded var
hithwen Oct 24, 2019
fb0cba8
Style fix
hithwen Oct 24, 2019
4240dc5
Merge remote-tracking branch 'origin/master' into julia/pg-refactor-u…
hithwen Oct 28, 2019
19f0d99
Update requirements when updating check
therve Oct 28, 2019
bcc3d40
Merge remote-tracking branch 'origin/therve/dev-requirements' into ju…
hithwen Oct 28, 2019
9e3b95a
Merge remote-tracking branch 'origin/master' into julia/pg-refactor-u…
hithwen Oct 28, 2019
faf3b65
Merge remote-tracking branch 'origin/master' into julia/pg-refactor-u…
hithwen Oct 29, 2019
0960c7a
Merge remote-tracking branch 'origin/master' into julia/pg-refactor-u…
hithwen Oct 29, 2019
689f485
Log error when cannot determine version
hithwen Oct 29, 2019
6c53573
Remove custom comparison methods
hithwen Oct 29, 2019
1ddb84f
Fix typo
hithwen Oct 30, 2019
d2efb46
Bump semver and throw exception if cannot determine version
hithwen Oct 30, 2019
7735c41
Add exception test
hithwen Oct 30, 2019
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
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ requests_ntlm==1.1.0
scandir==1.8
securesystemslib[crypto,pynacl]==0.11.3
selectors34==1.2.0; sys_platform == 'win32' and python_version < '3.4'
semver==2.9.0
serpent==1.27; sys_platform == 'win32'
service_identity[idna]==18.1.0
simplejson==3.6.5
Expand Down
99 changes: 24 additions & 75 deletions postgres/datadog_checks/postgres/postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import psycopg2
from six import iteritems
from six.moves import zip_longest

from datadog_checks.base import AgentCheck, ConfigurationError, is_affirmative

Expand Down Expand Up @@ -41,6 +40,7 @@
STATIO_METRICS,
fmt,
)
from .version_utils import V8_3, V9, V9_1, V9_2, V9_4, V9_6, V10, get_version

MAX_CUSTOM_RESULTS = 100
TABLE_COUNT_LIMIT = 200
Expand All @@ -64,6 +64,7 @@ def __init__(self, name, init_config, instances):
AgentCheck.__init__(self, name, init_config, instances)
self._clean_state()
self.db = None
self._version = None
self.custom_metrics = None

# Deprecate custom_metrics in favor of custom_queries
Expand Down Expand Up @@ -104,7 +105,7 @@ def _build_tags(self, custom_tags, host, port, dbname):
return tags

def _clean_state(self):
self.version = None
self._version = None
self.instance_metrics = None
self.bgw_metrics = None
self.archiver_metrics = None
Expand All @@ -120,62 +121,12 @@ def _get_replication_role(self):
# value fetched for role is of <type 'bool'>
return "standby" if role else "master"

def _get_version(self):
if self.version is None:
cursor = self.db.cursor()
cursor.execute('SHOW SERVER_VERSION;')
version = cursor.fetchone()[0]
try:
version_parts = version.split(' ')[0].split('.')
version = [int(part) for part in version_parts]
except Exception:
# Postgres might be in development, with format \d+[beta|rc]\d+
match = re.match(r'(\d+)([a-zA-Z]+)(\d+)', version)
if match:
version_parts = list(match.groups())

# We found a valid development version
if len(version_parts) == 3:
# Replace development tag with a negative number to properly compare versions
version_parts[1] = -1
version = [int(part) for part in version_parts]

self.version = version

self.service_metadata('version', self.version)
return self.version

def _is_above(self, version_to_compare):
version = self._get_version()
if type(version) == list:
# iterate from major down to bugfix
for v, vc in zip_longest(version, version_to_compare, fillvalue=0):
if v == vc:
continue

return v > vc

# return True if version is the same
return True
return False

def _is_8_3_or_above(self):
return self._is_above([8, 3, 0])

def _is_9_1_or_above(self):
return self._is_above([9, 1, 0])

def _is_9_2_or_above(self):
return self._is_above([9, 2, 0])

def _is_9_4_or_above(self):
return self._is_above([9, 4, 0])

def _is_9_6_or_above(self):
return self._is_above([9, 6, 0])

def _is_10_or_above(self):
return self._is_above([10, 0, 0])
@property
def version(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

We shouldn't cache version information as it may change

Copy link
Contributor Author

@hithwen hithwen Oct 29, 2019

Choose a reason for hiding this comment

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

The cache is cleared when wrong version is detected (see _clean_state). Take into account we are already caching version specific information such as which queries to run

Copy link
Contributor

@ofek ofek Oct 29, 2019

Choose a reason for hiding this comment

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

It's only cleared when an error occurs and the difference between minor releases is unlikely to cause one

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, that is right and that would affect inventories project only. So I'll remove the caching as part of the PR that adds inventories. This PR is not intending to change behaviour and version was already cached (#4874 (comment))

if self._version is None:
self._version = get_version(self.db)
self.service_metadata('version', [self._version.major, self._version.minor, self._version.patch])
hithwen marked this conversation as resolved.
Show resolved Hide resolved
return self._version

def _get_instance_metrics(self, database_size_metrics, collect_default_db):
"""
Expand All @@ -193,7 +144,7 @@ def _get_instance_metrics(self, database_size_metrics, collect_default_db):

if metrics is None:
# select the right set of metrics to collect depending on postgres version
if self._is_9_2_or_above():
if self.version >= V9_2:
self.instance_metrics = dict(COMMON_METRICS, **NEWER_92_METRICS)
else:
self.instance_metrics = dict(COMMON_METRICS)
Expand Down Expand Up @@ -242,11 +193,11 @@ def _get_bgw_metrics(self):
return None

self.db_bgw_metrics.append(sub_key)

self.bgw_metrics = dict(COMMON_BGW_METRICS)
if self._is_9_1_or_above():

if self.version >= V9_1:
self.bgw_metrics.update(NEWER_91_BGW_METRICS)
if self._is_9_2_or_above():
if self.version >= V9_2:
self.bgw_metrics.update(NEWER_92_BGW_METRICS)

metrics = self.bgw_metrics
Expand Down Expand Up @@ -277,7 +228,7 @@ def _get_archiver_metrics(self):
# the table, mirroring _get_bgw_metrics()
metrics = self.archiver_metrics

if metrics is None and self._is_9_4_or_above():
if metrics is None and self.version >= V9_4:
# Collect from only one instance. See _get_bgw_metrics() for details on why.
sub_key = self.key[:2]
if sub_key in self.db_archiver_metrics:
Expand Down Expand Up @@ -310,12 +261,12 @@ def _get_replication_metrics(self):
Uses a dictionnary to save the result for each instance
"""
metrics = self.replication_metrics
if self._is_10_or_above() and metrics is None:
if self.version >= V10 and metrics is None:
self.replication_metrics = dict(REPLICATION_METRICS_10)
metrics = self.replication_metrics
elif self._is_9_1_or_above() and metrics is None:
elif self.version >= V9_1 and metrics is None:
self.replication_metrics = dict(REPLICATION_METRICS_9_1)
if self._is_9_2_or_above():
if self.version >= V9_2:
self.replication_metrics.update(REPLICATION_METRICS_9_2)
metrics = self.replication_metrics
return metrics
Expand All @@ -328,12 +279,12 @@ def _get_activity_metrics(self, user):
metrics_data = self.activity_metrics

if metrics_data is None:
query = ACTIVITY_QUERY_10 if self._is_10_or_above() else ACTIVITY_QUERY_LT_10
if self._is_9_6_or_above():
query = ACTIVITY_QUERY_10 if self.version >= V10 else ACTIVITY_QUERY_LT_10
if self.version >= V9_6:
metrics_query = ACTIVITY_METRICS_9_6
elif self._is_9_2_or_above():
elif self.version >= V9_2:
metrics_query = ACTIVITY_METRICS_9_2
elif self._is_8_3_or_above():
elif self.version >= V8_3:
metrics_query = ACTIVITY_METRICS_8_3
else:
metrics_query = ACTIVITY_METRICS_LT_8_3
Expand Down Expand Up @@ -384,8 +335,7 @@ def _build_relations_config(self, yamlconfig):
def _query_scope(self, cursor, scope, instance_tags, is_custom_metrics, relations_config):
if scope is None:
return None

if scope == REPLICATION_METRICS or not self._is_above([9, 0, 0]):
if scope == REPLICATION_METRICS or not self.version >= V9:
log_func = self.log.debug
else:
log_func = self.log.warning
Expand Down Expand Up @@ -413,7 +363,7 @@ def _query_scope(self, cursor, scope, instance_tags, is_custom_metrics, relation
log_func(e)
log_func(
"It seems the PG version has been incorrectly identified as %s. "
"A reattempt to identify the right version will happen on next agent run." % self.version
"A reattempt to identify the right version will happen on next agent run." % self._version
)
self._clean_state()
self.db.rollback()
Expand Down Expand Up @@ -772,8 +722,7 @@ def check(self, instance):
self._connect(host, port, user, password, dbname, ssl, tags)
if tag_replication_role:
tags.extend(["replication_role:{}".format(self._get_replication_role())])
version = self._get_version()
self.log.debug("Running check against version %s" % version)
self.log.debug("Running check against version %s", self.version)
self._collect_stats(
user,
tags,
Expand Down
38 changes: 38 additions & 0 deletions postgres/datadog_checks/postgres/version_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# (C) Datadog, Inc. 2019
# All rights reserved
# Licensed under Simplified BSD License (see LICENSE)
import re

import semver

V8_3 = semver.parse("8.3.0")
V9 = semver.parse("9.0.0")
V9_1 = semver.parse("9.1.0")
V9_2 = semver.parse("9.2.0")
V9_4 = semver.parse("9.4.0")
V9_6 = semver.parse("9.6.0")
V10 = semver.parse("10.0.0")


def get_version(db):
cursor = db.cursor()
cursor.execute('SHOW SERVER_VERSION;')
raw_version = cursor.fetchone()[0]
try:
version_parts = raw_version.split(' ')[0].split('.')
version = [int(part) for part in version_parts]
while len(version) < 3:
version.append(0)
return semver.VersionInfo(*version)
except Exception:
# Postgres might be in development, with format \d+[beta|rc]\d+
match = re.match(r'(\d+)([a-zA-Z]+)(\d+)', raw_version)
Copy link
Contributor

Choose a reason for hiding this comment

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

is X.YbetaZ possible?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know. PG documentation only refers to actual releases, which are on X.Y.Z format https://www.postgresql.org/support/versioning/

Copy link
Member

Choose a reason for hiding this comment

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

According to https://github.com/postgres/postgres/releases/, postgres seems to follow semver, only betas differ from semver and they seem to only apply to major versions:
For example: postgres/postgres@a240570#diff-e2d5a00791bce9a01f99bc6fd613a39dR585

if match:
FlorianVeaux marked this conversation as resolved.
Show resolved Hide resolved
version = list(match.groups())
# We found a valid development version
if len(version) == 3:
# Replace development tag with a negative number to properly compare versions
version[1] = -1
version = [int(part) for part in version]
return semver.VersionInfo(*version)
raise Exception("Cannot determine which version is {}".format(raw_version))
1 change: 1 addition & 0 deletions postgres/requirements.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
psycopg2-binary==2.8.4
semver==2.9.0
5 changes: 2 additions & 3 deletions postgres/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
# Licensed under Simplified BSD License (see LICENSE)
import os

import mock
import psycopg2
import pytest
from semver import VersionInfo

from datadog_checks.dev import WaitFor, docker_run
from datadog_checks.postgres import PostgreSql
Expand All @@ -31,15 +31,14 @@ def dd_environment(e2e_instance):
@pytest.fixture
def check():
c = PostgreSql('postgres', {}, [{'dbname': 'dbname', 'host': 'localhost', 'port': '5432'}])
c._is_9_2_or_above = mock.MagicMock()
c._version = VersionInfo(9, 2, 0)
return c


@pytest.fixture
def integration_check():
def _check(instance):
c = PostgreSql('postgres', {}, [instance])
c._is_9_2_or_above = mock.MagicMock()
return c

return _check
Expand Down
9 changes: 5 additions & 4 deletions postgres/tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import mock
import psycopg2
import pytest
from semver import VersionInfo

from datadog_checks.postgres import PostgreSql

Expand Down Expand Up @@ -126,13 +127,13 @@ def test_activity_metrics(aggregator, integration_check, pg_instance):
def test_wrong_version(aggregator, integration_check, pg_instance):
check = integration_check(pg_instance)
# Enforce to cache wrong version
check.version = [9, 6, 0]
check._version = VersionInfo(*[9, 6, 0])

check.check(pg_instance)
assert_state_clean(check)

check.check(pg_instance)
assert check.version[0] == int(POSTGRES_VERSION)
assert check._version.major == int(POSTGRES_VERSION)
assert_state_set(check)


Expand All @@ -158,7 +159,7 @@ def throw_exception_first_time(*args, **kwargs):


def assert_state_clean(check):
assert check.version is None
assert check._version is None
assert check.instance_metrics is None
assert check.bgw_metrics is None
assert check.archiver_metrics is None
Expand All @@ -169,7 +170,7 @@ def assert_state_clean(check):


def assert_state_set(check,):
assert check.version
assert check._version
assert check.instance_metrics
assert check.bgw_metrics
if POSTGRES_VERSION != '9.3':
Expand Down
Loading