Skip to content

Commit f6dec88

Browse files
authored
Feature/datadog metrics (#1)
* metrics initial implementation Signed-off-by: Jorge Tapicha <jitapichab@gmail.com> Signed-off-by: jorge tapicha <jorge.tapicha@rappi.com>
1 parent 4a57bf4 commit f6dec88

File tree

6 files changed

+168
-0
lines changed

6 files changed

+168
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased][]
44

5+
### Added
6+
7+
* Metrics probe `chaosdatadog.metrics.get_metrics_state`
8+
59
[Unreleased]: https://github.com/chaostoolkit-incubator/chaostoolkit-datadog/compare/0.1.1...HEAD
610

711
## [0.1.1][]

chaosdatadog/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,6 @@ def load_exported_activities() -> List[DiscoveredActivities]:
6363
activities = [] # type: ignore
6464

6565
activities.extend(discover_probes("chaosdatadog.slo.probes"))
66+
activities.extend(discover_probes("chaosdatadog.metrics.probes"))
6667

6768
return activities

chaosdatadog/metrics/__init__.py

Whitespace-only changes.

chaosdatadog/metrics/probes.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from datetime import datetime
2+
3+
from chaoslib.exceptions import ActivityFailed
4+
from chaoslib.types import Configuration, Secrets
5+
from datadog_api_client.exceptions import ApiTypeError, NotFoundException
6+
from datadog_api_client.v1.api.metrics_api import MetricsApi
7+
from dateutil.relativedelta import relativedelta
8+
from logzero import logger
9+
10+
from chaosdatadog import get_client
11+
from chaosdatadog.metrics.utils import (
12+
check_comparison_values,
13+
extract_metric_name,
14+
get_comparison_operator,
15+
)
16+
17+
__all__ = ["get_metrics_state"]
18+
19+
20+
def get_metrics_state(
21+
query: str,
22+
comparison: str,
23+
threshold: float,
24+
minutes_before: int,
25+
configuration: Configuration = None,
26+
secrets: Secrets = None,
27+
) -> bool:
28+
"""
29+
The next function is to:
30+
31+
* Query metrics from any time period (timeseries and scalar)
32+
* Compare the metrics to some treshold in some time.
33+
Ex.(CPU, Memory, Network)
34+
* Check is the sum of datapoins is over some value.
35+
Ex. (requests, errors, custom metrics)
36+
37+
you can use a comparison to check if all data points in the query
38+
satisfy the steady state condition
39+
40+
Ex. cumsum(sum:istio.mesh.request.count.total{kube_service:test,
41+
response_code:500}.as_count())
42+
43+
the above query is a cumulative sum of all requests with response
44+
code of 500. if you want your request in a window of time
45+
you have a deviant hypothesis if you have more than 30 http_500 errors
46+
the comparison should be <. so any value below 30 is a steady state.
47+
48+
the allowed comparison values are [">", "<", ">=", "<=", "=="]
49+
50+
"""
51+
52+
try:
53+
check_comparison_values(comparison)
54+
except ValueError as e:
55+
raise ActivityFailed(e)
56+
57+
with get_client(configuration, secrets) as c:
58+
api = MetricsApi(c)
59+
60+
metric_name = extract_metric_name(query)
61+
62+
try:
63+
api.get_metric_metadata(metric_name)
64+
except NotFoundException as e:
65+
logger.debug(e)
66+
raise ActivityFailed("The metric name doesn't exist !")
67+
except ApiTypeError as e:
68+
logger.debug(e)
69+
raise ActivityFailed("The metric name wasn't in datadog format!")
70+
71+
metrics = api.query_metrics(
72+
_from=int(
73+
(
74+
datetime.now() + relativedelta(minutes=-minutes_before)
75+
).timestamp()
76+
),
77+
to=int(datetime.now().timestamp()),
78+
query=query,
79+
)
80+
81+
metrics = metrics.to_dict()
82+
series = metrics.get("series", [{}])
83+
if not series:
84+
point_list = [
85+
[datetime.now().timestamp(), 0],
86+
]
87+
series = [{"pointlist": point_list}]
88+
series = series[0] if len(series) > 0 else {}
89+
point_list = series.get("pointlist", [])
90+
point_value_list = [subpoints[1] for subpoints in point_list]
91+
compare_function = get_comparison_operator(comparison)
92+
return all(compare_function(_, threshold) for _ in point_value_list)

chaosdatadog/metrics/utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import re
2+
3+
COMPARISON_VALUES = [">", "<", ">=", "<=", "==", "!="]
4+
5+
6+
def extract_metric_name(query):
7+
pattern = r"(?::|^)([^{}:]+)(?:{|$)"
8+
match = re.search(pattern, query)
9+
return match[1] if match else None
10+
11+
12+
def check_comparison_values(comparison):
13+
if comparison not in COMPARISON_VALUES:
14+
raise ValueError(
15+
"Invalid value. Expected one of: '>', '<', '>=', '<=', '==', '!='"
16+
)
17+
18+
19+
def get_comparison_operator(comparison):
20+
operators = {
21+
">": lambda x, y: x > y,
22+
"<": lambda x, y: x < y,
23+
">=": lambda x, y: x >= y,
24+
"<=": lambda x, y: x <= y,
25+
"==": lambda x, y: x == y,
26+
"!=": lambda x, y: x != y,
27+
}
28+
return operators.get(comparison)

tests/test_metrics.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from datetime import datetime
2+
from unittest.mock import MagicMock, patch
3+
4+
from dateutil.relativedelta import relativedelta
5+
6+
from chaosdatadog.metrics.probes import get_metrics_state
7+
8+
9+
@patch("datadog_api_client.api_client.rest", autospec=False)
10+
def test_get_metrics_state(mock_get_client):
11+
query = "query"
12+
comparison = ">"
13+
threshold = 40
14+
minutes_before = 1
15+
16+
with patch("chaosdatadog.metrics.probes.MetricsApi") as MockMetricsApi:
17+
api_mock = MagicMock()
18+
MockMetricsApi.return_value = api_mock
19+
20+
point_list = [
21+
[datetime.now().timestamp(), 51],
22+
[datetime.now().timestamp(), 35],
23+
[datetime.now().timestamp(), 20],
24+
[datetime.now().timestamp(), 10],
25+
]
26+
series = {"pointlist": point_list}
27+
api_mock.query_metrics.return_value.to_dict.return_value = {
28+
"series": [series]
29+
}
30+
31+
result = get_metrics_state(query, comparison, threshold, minutes_before)
32+
33+
assert result is False
34+
35+
api_mock.query_metrics.assert_called_once_with(
36+
_from=int(
37+
(
38+
datetime.now() + relativedelta(minutes=-minutes_before)
39+
).timestamp()
40+
),
41+
to=int(datetime.now().timestamp()),
42+
query=query,
43+
)

0 commit comments

Comments
 (0)