Skip to content

Commit

Permalink
move to new tracking module
Browse files Browse the repository at this point in the history
  • Loading branch information
djova committed Dec 8, 2021
1 parent 7d8ffdc commit e2c6696
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 129 deletions.
68 changes: 0 additions & 68 deletions datadog_checks_base/datadog_checks/base/utils/db/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,71 +278,3 @@ def _run_job_rate_limited(self):

def run_job(self):
raise NotImplementedError()


def dbm_tracked_method(dbms, parent_check_attr=None, track_result_length=False):
"""
Decorates a function to provide debug metrics and logging for troubleshooting.
Tracks execution time, errors, and result length.
If the check has a `debug_stats_kwargs` function, that function is called to get a set of kwargs to pass to
the statsd methods (i.e. histogram, count, gauge, etc). This is useful when specific tags need to be added to
these debug metrics in a standardized way.
:param dbms: the dbms to report as (i.e. sqlserver)
:param parent_check_attr: the attribute of the class which references the check. If none, self must refer to the
check. The check reference is used to report debug metrics and to generate logs.
:param track_result_length: if true, the length of the result is tracked
:return: a decorator
"""

def decorator(function):
def wrapper(self, *args, **kwargs):
start_time = time.time()

try:
check = getattr(self, parent_check_attr) if parent_check_attr else self
except AttributeError:
print("[{} {}] invalid dbm_tracked_method. invalid check reference".format(dbms, function.__name__))
return function(self, *args, **kwargs)

stats_kwargs = {}
if hasattr(check, 'debug_stats_kwargs'):
stats_kwargs = dict(check.debug_stats_kwargs())

stats_kwargs['tags'] = stats_kwargs.get('tags', []) + ["operation:{}".format(function.__name__)]

try:
result = function(self, *args, **kwargs)

elapsed_ms = (time.time() - start_time) * 1000
check.histogram(
"dd.{}.operation.time".format(dbms),
elapsed_ms,
**stats_kwargs,
)

check.log.debug("[%s %s] operation completed in %s ms", elapsed_ms)

if track_result_length and result is not None:
check.log.debug("[%s %s] received result length %s", dbms, function.__name__, len(result))
check.gauge(
"dd.{}.operation.result.length".format(dbms),
len(result),
**stats_kwargs,
)

return result
except Exception as e:
check.log.exception("operation %s error", function.__name__)
stats_kwargs['tags'] += ["error:{}".format(type(e))]
check.count(
"dd.{}.operation.error".format(dbms),
1,
**stats_kwargs,
)
raise

return wrapper

return decorator
98 changes: 98 additions & 0 deletions datadog_checks_base/datadog_checks/base/utils/tracking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
# (C) Datadog, Inc. 2021-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)

import time

required_attrs = [
'log',
'count',
'gauge',
'histogram',
]


def tracked_method(namespace, agent_check_getter=None, track_result_length=False):
"""
Decorates a method to provide debug metrics and logging for troubleshooting.
Tracks execution time, errors, and result length.
The function being decorated must be a method on a class that receives the self pointer. This cannot decorate
plain functions.
If the check has a `debug_stats_kwargs` function, that function is called to get a set of kwargs to pass to
the statsd methods (i.e. histogram, count, gauge, etc). This is useful when specific tags need to be added to
these debug metrics in a standardized way.
:param namespace: the namespace to report as (i.e. if "sqlserver" then all metrics have the "dd.sqlserver.*" prefix)
:param agent_check_getter: a function that gets the agent check from the class. The function must receive only a
single parameter, `self`, and it must return a reference to the agent check. If the function is not provided then
`self` must refer to the agent check.
:param track_result_length: if true, the length of the result is tracked
:return: a decorator
"""

def decorator(function):
def wrapper(self, *args, **kwargs):
start_time = time.time()

try:
check = agent_check_getter(self) if agent_check_getter else self
except Exception:
print(
"[{}.{}] invalid dbm_tracked_method. failed to get check reference.".format(
namespace, function.__name__
)
)
return function(self, *args, **kwargs)

for attr in required_attrs:
if not hasattr(check, attr):
print(
"[{}.{}] invalid check reference. Missing required attribute {}.".format(
namespace, function.__name__, attr
)
)
return function(self, *args, **kwargs)

stats_kwargs = {}
if hasattr(check, 'debug_stats_kwargs'):
stats_kwargs = dict(check.debug_stats_kwargs())

stats_kwargs['tags'] = stats_kwargs.get('tags', []) + ["operation:{}".format(function.__name__)]

try:
result = function(self, *args, **kwargs)

elapsed_ms = (time.time() - start_time) * 1000
check.histogram(
"dd.{}.operation.time".format(namespace),
elapsed_ms,
**stats_kwargs,
)

check.log.debug("[%s.%s] operation completed in %s ms", elapsed_ms)

if track_result_length and result is not None:
check.log.debug("[%s.%s] received result length %s", namespace, function.__name__, len(result))
check.gauge(
"dd.{}.operation.result.length".format(namespace),
len(result),
**stats_kwargs,
)

return result
except Exception as e:
check.log.exception("operation %s error", function.__name__)
stats_kwargs['tags'] += ["error:{}".format(type(e))]
check.count(
"dd.{}.operation.error".format(namespace),
1,
**stats_kwargs,
)
raise

return wrapper

return decorator
62 changes: 1 addition & 61 deletions datadog_checks_base/tests/base/utils/db/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,7 @@

from datadog_checks.base import AgentCheck
from datadog_checks.base.stubs.datadog_agent import datadog_agent
from datadog_checks.base.utils.db.utils import (
ConstantRateLimiter,
DBMAsyncJob,
RateLimitingTTLCache,
dbm_tracked_method,
resolve_db_host,
)
from datadog_checks.base.utils.db.utils import ConstantRateLimiter, DBMAsyncJob, RateLimitingTTLCache, resolve_db_host


@pytest.mark.parametrize(
Expand Down Expand Up @@ -102,18 +96,6 @@ def run_job(self):
except Exception:
return

@dbm_tracked_method("test-dbms", parent_check_attr="_check")
def do_work(self):
return 5

@dbm_tracked_method("test-dbms", parent_check_attr="_check", track_result_length=True)
def do_work_return_list(self):
return list(range(5))

@dbm_tracked_method("test-dbms", parent_check_attr="_check")
def test_tracked_exception(self):
raise Exception("oops")


def test_dbm_async_job():
check = AgentCheck()
Expand Down Expand Up @@ -182,45 +164,3 @@ def test_dbm_async_job_inactive_stop(aggregator):
job.run_job_loop([])
job._job_loop_future.result()
aggregator.assert_metric("dd.test-dbms.async_job.inactive_stop", tags=['job:test-job'])


class TestAgentCheck(AgentCheck):
def __init__(self, debug_stats_kwargs):
self._debug_stats_kwargs = debug_stats_kwargs
super(TestAgentCheck, self).__init__()

def debug_stats_kwargs(self):
return self._debug_stats_kwargs


@pytest.mark.parametrize(
"debug_stats_kwargs",
[
{},
{
"tags": ["hey:there"],
"hostname": "tiberius",
},
],
)
def test_dbm_tracked_method(aggregator, debug_stats_kwargs):
check = TestAgentCheck(debug_stats_kwargs) if debug_stats_kwargs else AgentCheck()
job = TestJob(check, run_sync=True)
job.run_job_loop([])
assert job._job_loop_future is None

tags = debug_stats_kwargs.pop('tags', [])
hostname = debug_stats_kwargs.pop('hostname', None)

aggregator.assert_metric("dd.test-dbms.operation.time", hostname=hostname, tags=tags + ["operation:do_work"])
aggregator.assert_metric(
"dd.test-dbms.operation.time", hostname=hostname, tags=tags + ["operation:do_work_return_list"]
)
aggregator.assert_metric(
"dd.test-dbms.operation.result.length", hostname=hostname, tags=tags + ["operation:do_work_return_list"]
)
aggregator.assert_metric(
"dd.test-dbms.operation.error",
hostname=hostname,
tags=tags + ["operation:test_tracked_exception", "error:<class 'Exception'>"],
)
79 changes: 79 additions & 0 deletions datadog_checks_base/tests/base/utils/test_tracking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
# (C) Datadog, Inc. 2021-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)

import pytest

from datadog_checks.base import AgentCheck
from datadog_checks.base.utils.tracking import tracked_method


def agent_check_getter(self):
return self.check


class TestAgentCheck(AgentCheck):
def __init__(self, debug_stats_kwargs):
self._debug_stats_kwargs = debug_stats_kwargs
super(TestAgentCheck, self).__init__()

def debug_stats_kwargs(self):
return self._debug_stats_kwargs


class TestJob:
def __init__(self, check):
self.check = check

def run_job(self):
self.do_work()
self.do_work_return_list()
try:
self.test_tracked_exception()
except Exception:
return

@tracked_method("hello", agent_check_getter=agent_check_getter)
def do_work(self):
return 5

@tracked_method("hello", agent_check_getter=agent_check_getter, track_result_length=True)
def do_work_return_list(self):
return list(range(5))

@tracked_method("hello", agent_check_getter=agent_check_getter)
def test_tracked_exception(self):
raise Exception("oops")


@pytest.mark.parametrize(
"debug_stats_kwargs",
[
{},
{
"tags": ["hey:there"],
"hostname": "tiberius",
},
],
)
def test_tracked_method(aggregator, debug_stats_kwargs):
check = TestAgentCheck(debug_stats_kwargs) if debug_stats_kwargs else AgentCheck()
job = TestJob(check)
job.run_job()

tags = debug_stats_kwargs.pop('tags', [])
hostname = debug_stats_kwargs.pop('hostname', None)

aggregator.assert_metric("dd.hello.operation.time", hostname=hostname, tags=tags + ["operation:do_work"])
aggregator.assert_metric(
"dd.hello.operation.time", hostname=hostname, tags=tags + ["operation:do_work_return_list"]
)
aggregator.assert_metric(
"dd.hello.operation.result.length", hostname=hostname, tags=tags + ["operation:do_work_return_list"]
)
aggregator.assert_metric(
"dd.hello.operation.error",
hostname=hostname,
tags=tags + ["operation:test_tracked_exception", "error:<class 'Exception'>"],
)

0 comments on commit e2c6696

Please sign in to comment.