51
52
def _bound_duration(cls, alarm, constraints):
52
53
"""Bound the duration of the statistics query."""
53
54
now = timeutils.utcnow()
55
# when exclusion of weak datapoints is enabled, we extend
56
# the look-back period so as to allow a clearer sample count
57
# trend to be established
58
look_back = (cls.look_back if not alarm.rule.get('exclude_outliers')
59
else alarm.rule['evaluation_periods'])
54
60
window = (alarm.rule['period'] *
55
(alarm.rule['evaluation_periods'] + cls.look_back))
61
(alarm.rule['evaluation_periods'] + look_back))
56
62
start = now - datetime.timedelta(seconds=window)
57
63
LOG.debug(_('query stats from %(start)s to '
58
64
'%(now)s') % {'start': start, 'now': now})
65
71
def _sanitize(alarm, statistics):
66
72
"""Sanitize statistics.
67
Ultimately this will be the hook for the exclusion of chaotic
68
datapoints for example.
70
74
LOG.debug(_('sanitize stats %s') % statistics)
75
if alarm.rule.get('exclude_outliers'):
76
key = operator.attrgetter('count')
77
mean = utils.mean(statistics, key)
78
stddev = utils.stddev(statistics, key, mean)
79
lower = mean - 2 * stddev
80
upper = mean + 2 * stddev
81
inliers, outliers = utils.anomalies(statistics, key, lower, upper)
83
LOG.debug(_('excluded weak datapoints with sample counts %s'),
84
[s.count for s in outliers])
87
LOG.debug('no excluded weak datapoints')
71
89
# in practice statistics are always sorted by period start, not
72
90
# strictly required by the API though
73
statistics = statistics[:alarm.rule['evaluation_periods']]
91
statistics = statistics[-alarm.rule['evaluation_periods']:]
74
92
LOG.debug(_('pruned statistics to %d') % len(statistics))
93
111
if not sufficient and alarm.state != evaluator.UNKNOWN:
94
112
reason = _('%d datapoints are unknown') % alarm.rule[
95
113
'evaluation_periods']
96
self._refresh(alarm, evaluator.UNKNOWN, reason)
114
reason_data = self._reason_data('unknown',
115
alarm.rule['evaluation_periods'],
117
self._refresh(alarm, evaluator.UNKNOWN, reason, reason_data)
100
def _reason(alarm, statistics, distilled, state):
121
def _reason_data(disposition, count, most_recent):
122
"""Create a reason data dictionary for this evaluator type.
124
return {'type': 'threshold', 'disposition': disposition,
125
'count': count, 'most_recent': most_recent}
128
def _reason(cls, alarm, statistics, distilled, state):
101
129
"""Fabricate reason string."""
102
130
count = len(statistics)
103
131
disposition = 'inside' if state == evaluator.OK else 'outside'
104
132
last = getattr(statistics[-1], alarm.rule['statistic'])
105
133
transition = alarm.state != state
134
reason_data = cls._reason_data(disposition, count, last)
107
136
return (_('Transition to %(state)s due to %(count)d samples'
108
' %(disposition)s threshold, most recent: %(last)s') %
109
{'state': state, 'count': count,
110
'disposition': disposition, 'last': last})
137
' %(disposition)s threshold, most recent:'
139
% dict(reason_data, state=state)), reason_data
111
140
return (_('Remaining as %(state)s due to %(count)d samples'
112
' %(disposition)s threshold, most recent: %(last)s') %
113
{'state': state, 'count': count,
114
'disposition': disposition, 'last': last})
141
' %(disposition)s threshold, most recent: %(most_recent)s')
142
% dict(reason_data, state=state)), reason_data
116
144
def _transition(self, alarm, statistics, compared):
117
145
"""Transition alarm state if necessary.
135
163
state = evaluator.ALARM if distilled else evaluator.OK
136
reason = self._reason(alarm, statistics, distilled, state)
164
reason, reason_data = self._reason(alarm, statistics,
137
166
if alarm.state != state or continuous:
138
self._refresh(alarm, state, reason)
167
self._refresh(alarm, state, reason, reason_data)
139
168
elif unknown or continuous:
140
169
trending_state = evaluator.ALARM if compared[-1] else evaluator.OK
141
170
state = trending_state if unknown else alarm.state
142
reason = self._reason(alarm, statistics, distilled, state)
143
self._refresh(alarm, state, reason)
171
reason, reason_data = self._reason(alarm, statistics,
173
self._refresh(alarm, state, reason, reason_data)
145
175
def evaluate(self, alarm):
176
if not self.within_time_constraint(alarm):
177
LOG.debug(_('Attempted to evaluate alarm %s, but it is not '
178
'within its time constraint.') % alarm.alarm_id)
146
181
query = self._bound_duration(
148
183
alarm.rule['query']