Skip to content

Commit cf274d9

Browse files
authored
Fix: number based alerts evaluation isn't working (#4295)
* Fix: correctly evaluate numeric thresholds * Missing import * More missing imports * Alert evaluation: support for booleans
1 parent c004107 commit cf274d9

File tree

2 files changed

+68
-22
lines changed

2 files changed

+68
-22
lines changed

redash/models/__init__.py

+40-18
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import calendar
33
import logging
44
import time
5+
import numbers
56
import pytz
67

78
from six import text_type
@@ -775,6 +776,43 @@ def are_favorites(cls, user, objects):
775776
return [fav.object_id for fav in cls.query.filter(cls.object_id.in_([o.id for o in objects]), cls.object_type == object_type, cls.user_id == user)]
776777

777778

779+
OPERATORS = {
780+
'>': lambda v, t: v > t,
781+
'>=': lambda v, t: v >= t,
782+
'<': lambda v, t: v < t,
783+
'<=': lambda v, t: v <= t,
784+
'==': lambda v, t: v == t,
785+
'!=': lambda v, t: v != t,
786+
787+
# backward compatibility
788+
'greater than': lambda v, t: v > t,
789+
'less than': lambda v, t: v < t,
790+
'equals': lambda v, t: v == t,
791+
}
792+
793+
794+
def next_state(op, value, threshold):
795+
if isinstance(value, numbers.Number) and not isinstance(value, bool):
796+
try:
797+
threshold = float(threshold)
798+
except ValueError:
799+
return Alert.UNKNOWN_STATE
800+
# If it's a boolean cast to string and lower case, because upper cased
801+
# boolean value is Python specific and most likely will be confusing to
802+
# users.
803+
elif isinstance(value, bool):
804+
value = str(value).lower()
805+
else:
806+
value = str(value)
807+
808+
if op(value, threshold):
809+
new_state = Alert.TRIGGERED_STATE
810+
else:
811+
new_state = Alert.OK_STATE
812+
813+
return new_state
814+
815+
778816
@generic_repr('id', 'name', 'query_id', 'user_id', 'state', 'last_triggered_at', 'rearm')
779817
class Alert(TimestampMixin, BelongsToOrgMixin, db.Model):
780818
UNKNOWN_STATE = 'unknown'
@@ -819,28 +857,12 @@ def evaluate(self):
819857
data = self.query_rel.latest_query_data.data
820858

821859
if data['rows'] and self.options['column'] in data['rows'][0]:
822-
operators = {
823-
'>': lambda v, t: v > t,
824-
'>=': lambda v, t: v >= t,
825-
'<': lambda v, t: v < t,
826-
'<=': lambda v, t: v <= t,
827-
'==': lambda v, t: v == t,
828-
'!=': lambda v, t: v != t,
829-
830-
# backward compatibility
831-
'greater than': lambda v, t: v > t,
832-
'less than': lambda v, t: v < t,
833-
'equals': lambda v, t: v == t,
834-
}
835-
should_trigger = operators.get(self.options['op'], lambda v, t: False)
860+
op = OPERATORS.get(self.options['op'], lambda v, t: False)
836861

837862
value = data['rows'][0][self.options['column']]
838863
threshold = self.options['value']
839864

840-
if should_trigger(value, threshold):
841-
new_state = self.TRIGGERED_STATE
842-
else:
843-
new_state = self.OK_STATE
865+
new_state = next_state(op, value, threshold)
844866
else:
845867
new_state = self.UNKNOWN_STATE
846868

tests/models/test_alerts.py

+28-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
from unittest import TestCase
12
from tests import BaseTestCase
2-
from redash.models import Alert, db
3+
from redash.models import Alert, db, next_state, OPERATORS
34
from redash.utils import json_dumps
45

56

@@ -44,21 +45,44 @@ def get_results(value):
4445

4546

4647
class TestAlertEvaluate(BaseTestCase):
47-
def create_alert(self, results, column='foo'):
48+
def create_alert(self, results, column='foo', value="1"):
4849
result = self.factory.create_query_result(data=results)
4950
query = self.factory.create_query(latest_query_data_id=result.id)
50-
alert = self.factory.create_alert(query_rel=query, options={'op': 'equals', 'column': column, 'value': 1})
51+
alert = self.factory.create_alert(query_rel=query, options={'op': 'equals', 'column': column, 'value': value})
5152
return alert
5253

5354
def test_evaluate_triggers_alert_when_equal(self):
5455
alert = self.create_alert(get_results(1))
5556
self.assertEqual(alert.evaluate(), Alert.TRIGGERED_STATE)
5657

58+
def test_evaluate_number_value_and_string_threshold(self):
59+
alert = self.create_alert(get_results(1), value="string")
60+
self.assertEqual(alert.evaluate(), Alert.UNKNOWN_STATE)
61+
5762
def test_evaluate_return_unknown_when_missing_column(self):
5863
alert = self.create_alert(get_results(1), column='bar')
5964
self.assertEqual(alert.evaluate(), Alert.UNKNOWN_STATE)
6065

6166
def test_evaluate_return_unknown_when_empty_results(self):
6267
results = json_dumps({'rows': [], 'columns': [{'name': 'foo', 'type': 'STRING'}]})
6368
alert = self.create_alert(results)
64-
self.assertEqual(alert.evaluate(), Alert.UNKNOWN_STATE)
69+
self.assertEqual(alert.evaluate(), Alert.UNKNOWN_STATE)
70+
71+
72+
class TestNextState(TestCase):
73+
def test_numeric_value(self):
74+
self.assertEqual(Alert.TRIGGERED_STATE, next_state(OPERATORS.get('=='), 1, "1"))
75+
self.assertEqual(Alert.TRIGGERED_STATE, next_state(OPERATORS.get('=='), 1, "1.0"))
76+
77+
def test_numeric_value_and_plain_string(self):
78+
self.assertEqual(Alert.UNKNOWN_STATE, next_state(OPERATORS.get('=='), 1, "string"))
79+
80+
def test_non_numeric_value(self):
81+
self.assertEqual(Alert.OK_STATE, next_state(OPERATORS.get('=='), "1", "1.0"))
82+
83+
def test_string_value(self):
84+
self.assertEqual(Alert.TRIGGERED_STATE, next_state(OPERATORS.get('=='), "string", "string"))
85+
86+
def test_boolean_value(self):
87+
self.assertEqual(Alert.TRIGGERED_STATE, next_state(OPERATORS.get('=='), False, 'false'))
88+
self.assertEqual(Alert.TRIGGERED_STATE, next_state(OPERATORS.get('!='), False, 'true'))

0 commit comments

Comments
 (0)