Expose alarm state reason to API
We currently show no reason from the API point of view of why we don't have enough data to evaluate alarms or why we change the state of the alarm. This change exposes the reason we known to the API. Change-Id: Ic1fe95090339d39ad9638654db815aee41a7921e
This commit is contained in:
parent
343742cecf
commit
dfa63a5e4e
@ -74,6 +74,9 @@ state_kind_enum = wtypes.Enum(str, *state_kind)
|
||||
severity_kind = ["low", "moderate", "critical"]
|
||||
severity_kind_enum = wtypes.Enum(str, *severity_kind)
|
||||
|
||||
ALARM_REASON_DEFAULT = "Not evaluated yet"
|
||||
ALARM_REASON_MANUAL = "Manually set via API"
|
||||
|
||||
|
||||
class OverQuota(base.ClientSideError):
|
||||
def __init__(self, data):
|
||||
@ -250,6 +253,9 @@ class Alarm(base.Base):
|
||||
state_timestamp = datetime.datetime
|
||||
"The date of the last alarm state changed"
|
||||
|
||||
state_reason = wsme.wsattr(wtypes.text, default=ALARM_REASON_DEFAULT)
|
||||
"The reason of the current state"
|
||||
|
||||
severity = base.AdvEnum('severity', str, *severity_kind,
|
||||
default='low')
|
||||
"The severity of the alarm"
|
||||
@ -359,6 +365,7 @@ class Alarm(base.Base):
|
||||
timestamp=datetime.datetime(2015, 1, 1, 12, 0, 0, 0),
|
||||
state="ok",
|
||||
severity="moderate",
|
||||
state_reason="threshold over 90%",
|
||||
state_timestamp=datetime.datetime(2015, 1, 1, 12, 0, 0, 0),
|
||||
ok_actions=["http://site:8000/ok"],
|
||||
alarm_actions=["http://site:8000/alarm"],
|
||||
@ -620,8 +627,10 @@ class AlarmController(rest.RestController):
|
||||
data.timestamp = now
|
||||
if alarm_in.state != data.state:
|
||||
data.state_timestamp = now
|
||||
data.state_reason = ALARM_REASON_MANUAL
|
||||
else:
|
||||
data.state_timestamp = alarm_in.state_timestamp
|
||||
data.state_reason = alarm_in.state_reason
|
||||
|
||||
ALARMS_RULES[data.type].plugin.update_hook(data)
|
||||
|
||||
@ -699,8 +708,10 @@ class AlarmController(rest.RestController):
|
||||
now = timeutils.utcnow()
|
||||
alarm.state = state
|
||||
alarm.state_timestamp = now
|
||||
alarm.state_reason = ALARM_REASON_MANUAL
|
||||
alarm = pecan.request.storage.update_alarm(alarm)
|
||||
change = {'state': alarm.state}
|
||||
change = {'state': alarm.state,
|
||||
'state_reason': alarm.state_reason}
|
||||
self._record_change(change, now, on_behalf_of=alarm.project_id,
|
||||
type=models.AlarmChange.STATE_TRANSITION)
|
||||
return alarm.state
|
||||
@ -785,6 +796,7 @@ class AlarmsController(rest.RestController):
|
||||
|
||||
data.timestamp = now
|
||||
data.state_timestamp = now
|
||||
data.state_reason = ALARM_REASON_DEFAULT
|
||||
|
||||
ALARMS_RULES[data.type].plugin.create_hook(data)
|
||||
|
||||
|
@ -116,6 +116,7 @@ class Evaluator(object):
|
||||
try:
|
||||
previous = alarm.state
|
||||
alarm.state = state
|
||||
alarm.state_reason = reason
|
||||
if previous != state or always_record:
|
||||
LOG.info('alarm %(id)s transitioning to %(state)s because '
|
||||
'%(reason)s', {'id': alarm.alarm_id,
|
||||
|
@ -147,6 +147,7 @@ class Connection(base.Connection):
|
||||
project_id=row.project_id,
|
||||
state=row.state,
|
||||
state_timestamp=row.state_timestamp,
|
||||
state_reason=row.state_reason,
|
||||
ok_actions=row.ok_actions,
|
||||
alarm_actions=row.alarm_actions,
|
||||
insufficient_data_actions=(
|
||||
|
@ -51,6 +51,7 @@ class Alarm(base.Model):
|
||||
:param description: User friendly description of the alarm
|
||||
:param enabled: Is the alarm enabled
|
||||
:param state: Alarm state (ok/alarm/insufficient data)
|
||||
:param state_reason: Alarm state reason
|
||||
:param rule: A rule that defines when the alarm fires
|
||||
:param user_id: the owner/creator of the alarm
|
||||
:param project_id: the project_id of the creator
|
||||
@ -70,8 +71,9 @@ class Alarm(base.Model):
|
||||
"""
|
||||
def __init__(self, alarm_id, type, enabled, name, description,
|
||||
timestamp, user_id, project_id, state, state_timestamp,
|
||||
ok_actions, alarm_actions, insufficient_data_actions,
|
||||
repeat_actions, rule, time_constraints, severity=None):
|
||||
state_reason, ok_actions, alarm_actions,
|
||||
insufficient_data_actions, repeat_actions, rule,
|
||||
time_constraints, severity=None):
|
||||
if not isinstance(timestamp, datetime.datetime):
|
||||
raise TypeError(_("timestamp should be datetime object"))
|
||||
if not isinstance(state_timestamp, datetime.datetime):
|
||||
@ -88,6 +90,7 @@ class Alarm(base.Model):
|
||||
project_id=project_id,
|
||||
state=state,
|
||||
state_timestamp=state_timestamp,
|
||||
state_reason=state_reason,
|
||||
ok_actions=ok_actions,
|
||||
alarm_actions=alarm_actions,
|
||||
insufficient_data_actions=insufficient_data_actions,
|
||||
|
@ -0,0 +1,37 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2017 OpenStack Foundation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
"""add_reason_column
|
||||
|
||||
Revision ID: 6ae0d05d9451
|
||||
Revises: 367aadf5485f
|
||||
Create Date: 2017-06-05 16:42:42.379029
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '6ae0d05d9451'
|
||||
down_revision = '367aadf5485f'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('alarm', sa.Column('state_reason', sa.Text, nullable=True))
|
@ -94,6 +94,7 @@ class Alarm(Base):
|
||||
project_id = Column(String(128))
|
||||
|
||||
state = Column(String(255))
|
||||
state_reason = Column(Text)
|
||||
state_timestamp = Column(TimestampUTC,
|
||||
default=lambda: timeutils.utcnow())
|
||||
|
||||
|
@ -38,6 +38,7 @@ def default_alarms(auth_headers):
|
||||
alarm_id='a',
|
||||
description='a',
|
||||
state='insufficient data',
|
||||
state_reason='Not evaluated',
|
||||
severity='critical',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
@ -67,6 +68,7 @@ def default_alarms(auth_headers):
|
||||
alarm_id='b',
|
||||
description='b',
|
||||
state='insufficient data',
|
||||
state_reason='Not evaluated',
|
||||
severity='critical',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
@ -94,6 +96,7 @@ def default_alarms(auth_headers):
|
||||
alarm_id='c',
|
||||
description='c',
|
||||
state='insufficient data',
|
||||
state_reason='Not evaluated',
|
||||
severity='moderate',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
@ -221,6 +224,7 @@ class TestAlarms(TestAlarmsBase):
|
||||
alarm_id='c',
|
||||
description='c',
|
||||
state='ok',
|
||||
state_reason='Not evaluated',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
ok_actions=[],
|
||||
@ -298,6 +302,7 @@ class TestAlarms(TestAlarmsBase):
|
||||
alarm_id='c',
|
||||
description='c',
|
||||
state='insufficient data',
|
||||
state_reason='Not evaluated',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
ok_actions=[],
|
||||
@ -809,6 +814,7 @@ class TestAlarms(TestAlarmsBase):
|
||||
'enabled': False,
|
||||
'name': 'added_alarm',
|
||||
'state': 'ok',
|
||||
'state_reason': 'ignored',
|
||||
'type': 'threshold',
|
||||
'severity': 'low',
|
||||
'ok_actions': ['http://something/ok'],
|
||||
@ -841,6 +847,8 @@ class TestAlarms(TestAlarmsBase):
|
||||
# to check to IntegerType type conversion
|
||||
json['threshold_rule']['evaluation_periods'] = 3
|
||||
json['threshold_rule']['period'] = 180
|
||||
# to check it's read only
|
||||
json['state_reason'] = "Not evaluated yet"
|
||||
self._verify_alarm(json, alarms[0], 'added_alarm')
|
||||
|
||||
def test_post_alarm_outlier_exclusion_set(self):
|
||||
@ -1289,6 +1297,58 @@ class TestAlarms(TestAlarmsBase):
|
||||
self.assertEqual(['test://', 'log://'],
|
||||
alarms[0].alarm_actions)
|
||||
|
||||
def test_exercise_state_reason(self):
|
||||
body = {
|
||||
'name': 'nostate',
|
||||
'type': 'threshold',
|
||||
'threshold_rule': {
|
||||
'meter_name': 'ameter',
|
||||
'query': [{'field': 'metadata.field',
|
||||
'op': 'eq',
|
||||
'value': '5',
|
||||
'type': 'string'}],
|
||||
'comparison_operator': 'le',
|
||||
'statistic': 'count',
|
||||
'threshold': 50,
|
||||
'evaluation_periods': '3',
|
||||
'period': '180',
|
||||
},
|
||||
}
|
||||
headers = self.auth_headers
|
||||
headers['X-Roles'] = 'admin'
|
||||
|
||||
self.post_json('/alarms', params=body, status=201,
|
||||
headers=headers)
|
||||
alarms = list(self.alarm_conn.get_alarms(name='nostate'))
|
||||
self.assertEqual(1, len(alarms))
|
||||
alarm_id = alarms[0].alarm_id
|
||||
|
||||
alarm = self._get_alarm(alarm_id)
|
||||
self.assertEqual("insufficient data", alarm['state'])
|
||||
self.assertEqual("Not evaluated yet", alarm['state_reason'])
|
||||
|
||||
# Ensure state reason is updated
|
||||
alarm = self._get_alarm('a')
|
||||
alarm['state'] = 'ok'
|
||||
self.put_json('/alarms/%s' % alarm_id,
|
||||
params=alarm,
|
||||
headers=self.auth_headers)
|
||||
alarm = self._get_alarm(alarm_id)
|
||||
self.assertEqual("ok", alarm['state'])
|
||||
self.assertEqual("Manually set via API", alarm['state_reason'])
|
||||
|
||||
# Ensure state reason read only
|
||||
alarm = self._get_alarm('a')
|
||||
alarm['state'] = 'alarm'
|
||||
alarm['state_reason'] = 'oh no!'
|
||||
self.put_json('/alarms/%s' % alarm_id,
|
||||
params=alarm,
|
||||
headers=self.auth_headers)
|
||||
|
||||
alarm = self._get_alarm(alarm_id)
|
||||
self.assertEqual("alarm", alarm['state'])
|
||||
self.assertEqual("Manually set via API", alarm['state_reason'])
|
||||
|
||||
def test_post_alarm_without_actions(self):
|
||||
body = {
|
||||
'name': 'alarm_actions_none',
|
||||
@ -1641,6 +1701,8 @@ class TestAlarms(TestAlarmsBase):
|
||||
alarms = list(self.alarm_conn.get_alarms(alarm_id=data[0]['alarm_id']))
|
||||
self.assertEqual(1, len(alarms))
|
||||
self.assertEqual('alarm', alarms[0].state)
|
||||
self.assertEqual('Manually set via API',
|
||||
alarms[0].state_reason)
|
||||
self.assertEqual('alarm', resp.json)
|
||||
|
||||
def test_set_invalid_state_alarm(self):
|
||||
@ -1726,6 +1788,7 @@ class TestAlarmsHistory(TestAlarmsBase):
|
||||
alarm_id='a',
|
||||
description='a',
|
||||
state='insufficient data',
|
||||
state_reason='insufficient data',
|
||||
severity='critical',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
@ -1764,7 +1827,10 @@ class TestAlarmsHistory(TestAlarmsBase):
|
||||
|
||||
def _assert_is_subset(self, expected, actual):
|
||||
for k, v in six.iteritems(expected):
|
||||
self.assertEqual(v, actual.get(k), 'mismatched field: %s' % k)
|
||||
current = actual.get(k)
|
||||
if k == 'detail' and isinstance(v, dict):
|
||||
current = jsonutils.loads(current)
|
||||
self.assertEqual(v, current, 'mismatched field: %s' % k)
|
||||
self.assertIsNotNone(actual['event_id'])
|
||||
|
||||
def _assert_in_json(self, expected, actual):
|
||||
@ -1955,7 +2021,9 @@ class TestAlarmsHistory(TestAlarmsBase):
|
||||
auth_headers=auth)
|
||||
self.assertEqual(2, len(history), 'hist: %s' % history)
|
||||
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
|
||||
detail='{"state": "alarm"}',
|
||||
detail={"state": "alarm",
|
||||
"state_reason":
|
||||
"Manually set via API"},
|
||||
on_behalf_of=alarm['project_id'],
|
||||
project_id=admin_project,
|
||||
type='rule change',
|
||||
@ -2405,6 +2473,7 @@ class TestAlarmsRuleGnocchi(TestAlarmsBase):
|
||||
alarm_id='e',
|
||||
description='e',
|
||||
state='insufficient data',
|
||||
state_reason='Not evaluated',
|
||||
severity='critical',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
@ -2432,6 +2501,7 @@ class TestAlarmsRuleGnocchi(TestAlarmsBase):
|
||||
alarm_id='f',
|
||||
description='f',
|
||||
state='insufficient data',
|
||||
state_reason='Not evaluated',
|
||||
severity='critical',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
@ -2458,6 +2528,7 @@ class TestAlarmsRuleGnocchi(TestAlarmsBase):
|
||||
alarm_id='g',
|
||||
description='f',
|
||||
state='insufficient data',
|
||||
state_reason='Not evaluated',
|
||||
severity='critical',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
@ -2671,6 +2742,7 @@ class TestAlarmsEvent(TestAlarmsBase):
|
||||
alarm_id='h',
|
||||
description='h',
|
||||
state='insufficient data',
|
||||
state_reason='insufficient data',
|
||||
severity='moderate',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
@ -2796,6 +2868,7 @@ class TestAlarmsCompositeRule(TestAlarmsBase):
|
||||
alarm_id='composite',
|
||||
description='composite',
|
||||
state='insufficient data',
|
||||
state_reason='insufficient data',
|
||||
severity='moderate',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
|
@ -49,6 +49,7 @@ class TestQueryAlarmsController(tests_api.FunctionalTest):
|
||||
alarm_id=alarm_id,
|
||||
description='a',
|
||||
state=state,
|
||||
state_reason="state_reason",
|
||||
state_timestamp=date,
|
||||
timestamp=date,
|
||||
ok_actions=[],
|
||||
|
@ -56,6 +56,7 @@ class AlarmTestBase(DBTestBase):
|
||||
user_id='me',
|
||||
project_id='and-da-boys',
|
||||
state="insufficient data",
|
||||
state_reason="insufficient data",
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
ok_actions=[],
|
||||
alarm_actions=['http://nowhere/alarms'],
|
||||
@ -85,6 +86,7 @@ class AlarmTestBase(DBTestBase):
|
||||
user_id='me',
|
||||
project_id='and-da-boys',
|
||||
state="insufficient data",
|
||||
state_reason="insufficient data",
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
ok_actions=[],
|
||||
alarm_actions=['http://nowhere/alarms'],
|
||||
@ -112,6 +114,7 @@ class AlarmTestBase(DBTestBase):
|
||||
user_id='me',
|
||||
project_id='and-da-boys',
|
||||
state="insufficient data",
|
||||
state_reason="insufficient data",
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
ok_actions=[],
|
||||
alarm_actions=['http://nowhere/alarms'],
|
||||
@ -219,6 +222,7 @@ class AlarmTest(AlarmTestBase):
|
||||
user_id='bla',
|
||||
project_id='ffo',
|
||||
state="insufficient data",
|
||||
state_reason="insufficient data",
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
ok_actions=[],
|
||||
alarm_actions=[],
|
||||
|
@ -170,6 +170,7 @@ class CompositeTest(BaseCompositeEvaluate):
|
||||
project_id='fake_project',
|
||||
alarm_id=uuidutils.generate_uuid(),
|
||||
state='insufficient data',
|
||||
state_reason='insufficient data',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
insufficient_data_actions=[],
|
||||
@ -190,6 +191,7 @@ class CompositeTest(BaseCompositeEvaluate):
|
||||
user_id='fake_user',
|
||||
project_id='fake_project',
|
||||
state='insufficient data',
|
||||
state_reason='insufficient data',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
insufficient_data_actions=[],
|
||||
@ -211,6 +213,7 @@ class CompositeTest(BaseCompositeEvaluate):
|
||||
user_id='fake_user',
|
||||
project_id='fake_project',
|
||||
state='insufficient data',
|
||||
state_reason='insufficient data',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
insufficient_data_actions=[],
|
||||
@ -233,6 +236,7 @@ class CompositeTest(BaseCompositeEvaluate):
|
||||
project_id='fake_project',
|
||||
alarm_id=uuidutils.generate_uuid(),
|
||||
state='insufficient data',
|
||||
state_reason='insufficient data',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
insufficient_data_actions=[],
|
||||
@ -456,6 +460,7 @@ class OtherCompositeTest(BaseCompositeEvaluate):
|
||||
user_id='fake_user',
|
||||
project_id='fake_project',
|
||||
state='insufficient data',
|
||||
state_reason='insufficient data',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
insufficient_data_actions=['log://'],
|
||||
|
@ -41,6 +41,7 @@ class TestEventAlarmEvaluate(base.TestEvaluatorBase):
|
||||
alarm_id=alarm_id,
|
||||
description='desc',
|
||||
state=kwargs.get('state', 'insufficient data'),
|
||||
state_reason='reason',
|
||||
severity='critical',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
|
@ -45,6 +45,7 @@ class TestGnocchiEvaluatorBase(base.TestEvaluatorBase):
|
||||
project_id='snafu',
|
||||
alarm_id=uuidutils.generate_uuid(),
|
||||
state='insufficient data',
|
||||
state_reason='insufficient data',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
insufficient_data_actions=[],
|
||||
@ -69,6 +70,7 @@ class TestGnocchiEvaluatorBase(base.TestEvaluatorBase):
|
||||
user_id='foobar',
|
||||
project_id='snafu',
|
||||
state='insufficient data',
|
||||
state_reason='insufficient data',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
insufficient_data_actions=[],
|
||||
@ -94,6 +96,7 @@ class TestGnocchiEvaluatorBase(base.TestEvaluatorBase):
|
||||
project_id='snafu',
|
||||
alarm_id=uuidutils.generate_uuid(),
|
||||
state='insufficient data',
|
||||
state_reason='insufficient data',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
insufficient_data_actions=[],
|
||||
|
@ -47,6 +47,7 @@ class TestEvaluate(base.TestEvaluatorBase):
|
||||
alarm_id=uuidutils.generate_uuid(),
|
||||
state='insufficient data',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
state_reason='Not evaluated',
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
insufficient_data_actions=[],
|
||||
ok_actions=[],
|
||||
@ -76,6 +77,7 @@ class TestEvaluate(base.TestEvaluatorBase):
|
||||
project_id='snafu',
|
||||
state='insufficient data',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
state_reason='Not evaluated',
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
insufficient_data_actions=[],
|
||||
ok_actions=[],
|
||||
@ -419,6 +421,7 @@ class TestEvaluate(base.TestEvaluatorBase):
|
||||
primitive_alarms = [a.as_dict() for a in self.alarms]
|
||||
for alarm in original_alarms:
|
||||
alarm.state = 'alarm'
|
||||
alarm.state_reason = mock.ANY
|
||||
primitive_original_alarms = [a.as_dict() for a in original_alarms]
|
||||
self.assertEqual(primitive_original_alarms, primitive_alarms)
|
||||
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
The reason of the state change is now part of the API as "state_reason" field of the alarm object.
|
Loading…
Reference in New Issue
Block a user