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

Add ability to submit time deltas to database query utility #5524

Merged
merged 2 commits into from
Jan 22, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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 @@ -50,6 +50,7 @@ pysocks==1.7.0
python-binary-memcached==0.26.1; sys_platform != 'win32'
python-dateutil==2.8.0
python3-gearman==0.1.0; sys_platform != 'win32' and python_version > '3.0'
pytz==2019.3
pyvmomi==v6.5.0.2017.5-1
pywin32==227; sys_platform == 'win32'
pyyaml==5.1
Expand Down
26 changes: 25 additions & 1 deletion datadog_checks_base/datadog_checks/base/utils/db/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
from __future__ import division

import re
from datetime import datetime

from ... import is_affirmative
from ...constants import ServiceCheck
from .. import constants
from ..common import compute_percent, total_time_to_temporal_percent
from .utils import create_extra_transformer
from .utils import create_extra_transformer, normalize_datetime

# Used for the user-defined `expression`s
ALLOWED_GLOBALS = {
Expand Down Expand Up @@ -96,6 +97,28 @@ def service_check(_, value, **kwargs):
return service_check


def get_time_elapsed(transformers, column_name, **modifiers):
mgarabed marked this conversation as resolved.
Show resolved Hide resolved
time_format = modifiers.pop('format', 'native')
if not isinstance(time_format, str):
raise ValueError('the `format` parameter must be a string')

gauge = transformers['gauge'](transformers, column_name, **modifiers)

if time_format == 'native':

def time_elapsed(_, value, **kwargs):
value = normalize_datetime(value)
gauge(_, (datetime.now(value.tzinfo) - value).total_seconds(), **kwargs)

else:

def time_elapsed(_, value, **kwargs):
value = normalize_datetime(datetime.strptime(value, time_format))
gauge(_, (datetime.now(value.tzinfo) - value).total_seconds(), **kwargs)

return time_elapsed


def get_expression(transformers, name, **modifiers):
available_sources = modifiers.pop('sources')

Expand Down Expand Up @@ -183,6 +206,7 @@ def percent(sources, **kwargs):
'tag': get_tag,
'match': get_match,
'service_check': get_service_check,
'time_elapsed': get_time_elapsed,
}

EXTRA_TRANSFORMERS = {'expression': get_expression, 'percent': get_percent}
Expand Down
11 changes: 11 additions & 0 deletions datadog_checks_base/datadog_checks/base/utils/db/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# Licensed under a 3-clause BSD style license (see LICENSE)
from itertools import chain

import pytz

# AgentCheck methods to transformer name e.g. set_metadata -> metadata
SUBMISSION_METHODS = {
'gauge': 'gauge',
Expand Down Expand Up @@ -52,3 +54,12 @@ def transformer(sources, **kwargs):
transformer = column_transformer

return transformer


def normalize_datetime(dt):
# Prevent naive datetime objects
if dt.tzinfo is None:
# The stdlib datetime.timezone.utc doesn't work properly on Windows
dt = dt.replace(tzinfo=pytz.utc)
Copy link
Member

Choose a reason for hiding this comment

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

We are assuming that since this is coming from a database, its most likely UTC which makes sense.

There are occasions if we have a naive timestamp that we should normalize to local timezone instead, maybe an opportunity for a future PR timeutils module.

An implementation I have used in the past which has been quite handy: https://github.com/riverbed/steelscript/blob/af4405c63f378db5466397fd566beb8226edf647/steelscript/common/timeutils.py#L52


return dt
1 change: 1 addition & 0 deletions datadog_checks_base/requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ kubernetes==8.0.1
prometheus-client==0.3.0
protobuf==3.7.0
pysocks==1.7.0
pytz==2019.3
pywin32==227; sys_platform == 'win32'
pyyaml==5.1
requests==2.22.0
Expand Down
160 changes: 160 additions & 0 deletions datadog_checks_base/tests/test_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
import logging
from datetime import datetime, timedelta, tzinfo

import pytest
import pytz

from datadog_checks.base import AgentCheck
from datadog_checks.base.stubs.aggregator import AggregatorStub
Expand All @@ -30,6 +32,17 @@ def create_query_manager(*args, **kwargs):
return QueryManager(check, executor, [Query(arg) for arg in args], **kwargs)


class EST(tzinfo):
mgarabed marked this conversation as resolved.
Show resolved Hide resolved
def utcoffset(self, dt):
return timedelta(hours=-5) + self.dst(dt)

def dst(self, dt):
return timedelta(0)

def tzname(self, dt):
return 'Eastern Standard Time'


class TestQueryResultIteration:
def test_executor_empty_result(self):
query_manager = create_query_manager()
Expand Down Expand Up @@ -913,6 +926,25 @@ def test_service_check_status_map_status_invalid(self):
):
query_manager.compile_queries()

def test_time_elapsed_format_not_string(self):
query_manager = create_query_manager(
{
'name': 'test query',
'query': 'foo',
'columns': [{'name': 'test.foo', 'type': 'time_elapsed', 'format': 5}],
'tags': ['test:bar'],
}
)

with pytest.raises(
ValueError,
match=(
'^error compiling type `time_elapsed` for column test.foo of test query: '
'the `format` parameter must be a string$'
),
):
query_manager.compile_queries()


class TestSubmission:
@pytest.mark.parametrize(
Expand Down Expand Up @@ -1386,6 +1418,134 @@ def test_service_check_unknown(self, aggregator):
aggregator.assert_service_check('test.foo', 3, message='baz', tags=['test:foo', 'test:bar'])
aggregator.assert_all_metrics_covered()

def test_time_elapsed_native(self, aggregator):
query_manager = create_query_manager(
{
'name': 'test query',
'query': 'foo',
'columns': [
{'name': 'test', 'type': 'tag'},
{'name': 'test.foo', 'type': 'time_elapsed', 'format': 'native'},
],
'tags': ['test:bar'],
},
executor=mock_executor([['tag1', datetime.now(pytz.utc) + timedelta(hours=-1)]]),
tags=['test:foo'],
)
query_manager.compile_queries()
query_manager.execute()

assert 'test.foo' in aggregator._metrics
assert len(aggregator._metrics) == 1
assert len(aggregator._metrics['test.foo']) == 1
m = aggregator._metrics['test.foo'][0]

assert 3599 < m.value < 3601
assert m.type == aggregator.GAUGE
assert m.tags == ['test:foo', 'test:bar', 'test:tag1']

def test_time_elapsed_native_default(self, aggregator):
query_manager = create_query_manager(
{
'name': 'test query',
'query': 'foo',
'columns': [{'name': 'test', 'type': 'tag'}, {'name': 'test.foo', 'type': 'time_elapsed'}],
'tags': ['test:bar'],
},
executor=mock_executor([['tag1', datetime.now(pytz.utc) + timedelta(hours=-1)]]),
tags=['test:foo'],
)
query_manager.compile_queries()
query_manager.execute()

assert 'test.foo' in aggregator._metrics
assert len(aggregator._metrics) == 1
assert len(aggregator._metrics['test.foo']) == 1
m = aggregator._metrics['test.foo'][0]

assert 3599 < m.value < 3601
assert m.type == aggregator.GAUGE
assert m.tags == ['test:foo', 'test:bar', 'test:tag1']

def test_time_elapsed_format(self, aggregator):
time_format = '%Y-%m-%dT%H-%M-%S%Z'
query_manager = create_query_manager(
{
'name': 'test query',
'query': 'foo',
'columns': [
{'name': 'test', 'type': 'tag'},
{'name': 'test.foo', 'type': 'time_elapsed', 'format': time_format},
],
'tags': ['test:bar'],
},
executor=mock_executor([['tag1', (datetime.now(pytz.utc) + timedelta(hours=-1)).strftime(time_format)]]),
tags=['test:foo'],
)
query_manager.compile_queries()
query_manager.execute()

assert 'test.foo' in aggregator._metrics
assert len(aggregator._metrics) == 1
assert len(aggregator._metrics['test.foo']) == 1
m = aggregator._metrics['test.foo'][0]

assert 3599 < m.value < 3601
assert m.type == aggregator.GAUGE
assert m.tags == ['test:foo', 'test:bar', 'test:tag1']

def test_time_elapsed_datetime_naive(self, aggregator):
query_manager = create_query_manager(
{
'name': 'test query',
'query': 'foo',
'columns': [
{'name': 'test', 'type': 'tag'},
{'name': 'test.foo', 'type': 'time_elapsed', 'format': 'native'},
],
'tags': ['test:bar'],
},
executor=mock_executor([['tag1', datetime.utcnow() + timedelta(hours=-1)]]),
tags=['test:foo'],
)
query_manager.compile_queries()
query_manager.execute()

assert 'test.foo' in aggregator._metrics
assert len(aggregator._metrics) == 1
assert len(aggregator._metrics['test.foo']) == 1
m = aggregator._metrics['test.foo'][0]

assert 3599 < m.value < 3601
assert m.type == aggregator.GAUGE
assert m.tags == ['test:foo', 'test:bar', 'test:tag1']

def test_time_elapsed_datetime_aware(self, aggregator):
query_manager = create_query_manager(
{
'name': 'test query',
'query': 'foo',
'columns': [
{'name': 'test', 'type': 'tag'},
{'name': 'test.foo', 'type': 'time_elapsed', 'format': 'native'},
],
'tags': ['test:bar'],
},
executor=mock_executor([['tag1', datetime.now(EST()) + timedelta(hours=-1)]]),
tags=['test:foo'],
)
query_manager.compile_queries()
query_manager.execute()

assert 'test.foo' in aggregator._metrics
assert len(aggregator._metrics) == 1
assert len(aggregator._metrics['test.foo']) == 1
m = aggregator._metrics['test.foo'][0]

assert 3599 < m.value < 3601
assert m.type == aggregator.GAUGE
assert m.tags == ['test:foo', 'test:bar', 'test:tag1']


class TestExtraTransformers:
def test_expression(self, aggregator):
Expand Down