From 87315c31c670ba668621239b1b1ae87862268376 Mon Sep 17 00:00:00 2001 From: ostro Date: Wed, 24 Oct 2018 19:40:13 +0200 Subject: [PATCH 01/38] example alarm logic --- overwatch/processing/alarms/__init__.py | 0 overwatch/processing/alarms/alarm.py | 27 ++++++++++ overwatch/processing/alarms/andAlarm.py | 20 ++++++++ overwatch/processing/alarms/boarderAlarm.py | 17 +++++++ overwatch/processing/alarms/collectors.py | 22 ++++++++ overwatch/processing/alarms/test_alarms.py | 50 +++++++++++++++++++ .../processing/alarms/totalTrendErrorAlarm.py | 15 ++++++ 7 files changed, 151 insertions(+) create mode 100644 overwatch/processing/alarms/__init__.py create mode 100644 overwatch/processing/alarms/alarm.py create mode 100644 overwatch/processing/alarms/andAlarm.py create mode 100644 overwatch/processing/alarms/boarderAlarm.py create mode 100644 overwatch/processing/alarms/collectors.py create mode 100644 overwatch/processing/alarms/test_alarms.py create mode 100644 overwatch/processing/alarms/totalTrendErrorAlarm.py diff --git a/overwatch/processing/alarms/__init__.py b/overwatch/processing/alarms/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/overwatch/processing/alarms/alarm.py b/overwatch/processing/alarms/alarm.py new file mode 100644 index 00000000..9abe2252 --- /dev/null +++ b/overwatch/processing/alarms/alarm.py @@ -0,0 +1,27 @@ +try: + from typing import * # noqa +except ImportError: + pass +else: + from ..trending.objects.object import TrendingObject # noqa + + +class Alarm: + def __init__(self, *args, alarmText='', **kwargs): + self.alarmText = alarmText + self.receivers = [] + + def addReceiver(self, receiver): # type: (callable) -> None + self.receivers.append(receiver) + + def checkAlarm(self, trend): # type: (TrendingObject) -> bool + """abstract method""" + raise NotImplementedError + + def announceAlarm(self, msg): + for receiver in self.receivers: + receiver(msg) + + def formatMessage(self, trend='', msg=''): # type: (TrendingObject) -> str + return "{alarmText} {trend} {msg}".format( + alarmText=self.alarmText, trend=trend, msg=msg) diff --git a/overwatch/processing/alarms/andAlarm.py b/overwatch/processing/alarms/andAlarm.py new file mode 100644 index 00000000..e5981d10 --- /dev/null +++ b/overwatch/processing/alarms/andAlarm.py @@ -0,0 +1,20 @@ +from .alarm import Alarm + +try: + from typing import * # noqa +except ImportError: + pass + + +class AndAlarm(Alarm): + def __init__(self, children=None, *args, **kwargs): # type: (List[Alarm]) -> None + super(AndAlarm, self).__init__(*args, **kwargs) + self.children = [] if not children else children + + def checkAlarm(self, trend): + for child in self.children: + if not child.checkAlarm(trend): + return False + + self.announceAlarm(self.formatMessage(trend)) + return True diff --git a/overwatch/processing/alarms/boarderAlarm.py b/overwatch/processing/alarms/boarderAlarm.py new file mode 100644 index 00000000..a423d14d --- /dev/null +++ b/overwatch/processing/alarms/boarderAlarm.py @@ -0,0 +1,17 @@ +from .alarm import Alarm + + +class BorderAlarm(Alarm): + def __init__(self, minVal=0, maxVal=100, *args, **kwargs): + super(BorderAlarm, self).__init__(*args, **kwargs) + self.minVal = minVal + self.maxVal = maxVal + + def checkAlarm(self, trend): + testedValue = trend.trendedValues[-1] + if self.minVal <= testedValue <= self.maxVal: + return False + + alarm = "value: {} not in [{}, {}]".format(testedValue, self.minVal, self.maxVal) + self.announceAlarm(self.formatMessage(trend, alarm)) + return True diff --git a/overwatch/processing/alarms/collectors.py b/overwatch/processing/alarms/collectors.py new file mode 100644 index 00000000..606d7674 --- /dev/null +++ b/overwatch/processing/alarms/collectors.py @@ -0,0 +1,22 @@ + + +def printCollector(alarm): + print(alarm) + + +class MailSender: + def __init__(self, address): + self.address = address + + def __call__(self, alarm): + self.sendMail(alarm) + + def sendMail(self, payload): + printCollector("MAIL TO:{} FROM:alarm@overwatch PAYLOAD:'{}'".format(self.address, payload)) + + +def httpCollector(alarm): + printCollector("HTTP: {}".format(alarm)) + + +workerMail = MailSender("worker@cern") diff --git a/overwatch/processing/alarms/test_alarms.py b/overwatch/processing/alarms/test_alarms.py new file mode 100644 index 00000000..3e330293 --- /dev/null +++ b/overwatch/processing/alarms/test_alarms.py @@ -0,0 +1,50 @@ +from overwatch.processing.alarms.collectors import workerMail, printCollector, httpCollector, MailSender +from overwatch.processing.alarms.andAlarm import AndAlarm +from overwatch.processing.alarms.boarderAlarm import BorderAlarm +from overwatch.processing.alarms.totalTrendErrorAlarm import TotalTrendErrorAlarm + + +class TrendingObjectMock: + def __init__(self, alarms): + self.alarms = alarms + self.trendedValues = [] + + def addNewValue(self, val): + self.trendedValues.append(val) + self.checkAlarms() + + def checkAlarms(self): + for alarm in self.alarms: + alarm.checkAlarm(self) + + def __str__(self): + return self.__class__.__name__ + + +def alarmConfig(): + boarderWarning = BorderAlarm(maxVal=50, alarmText="WARNING") + boarderWarning.addReceiver(printCollector) + + borderError = BorderAlarm(maxVal=70, alarmText="ERROR") + borderError.receivers = [workerMail, httpCollector] + + borderAlarm = BorderAlarm(maxVal=90) + totalAlarm = TotalTrendErrorAlarm(limit=1000) + seriousAlarm = AndAlarm([borderAlarm, totalAlarm], alarmText="Serious Alarm") + cernBoss = MailSender("boss@cern") + seriousAlarm.addReceiver(cernBoss) + + return [boarderWarning, borderError, seriousAlarm] + + +def main(): + to = TrendingObjectMock(alarmConfig()) + + values = [3, 14, 60, 80, 8888, 7, 95] + for i, val in enumerate(values): + print("\nVal number: {i} New value:{val}".format(i=i, val=val)) + to.addNewValue(val) + + +if __name__ == '__main__': + main() diff --git a/overwatch/processing/alarms/totalTrendErrorAlarm.py b/overwatch/processing/alarms/totalTrendErrorAlarm.py new file mode 100644 index 00000000..524dc805 --- /dev/null +++ b/overwatch/processing/alarms/totalTrendErrorAlarm.py @@ -0,0 +1,15 @@ +from .alarm import Alarm + + +class TotalTrendErrorAlarm(Alarm): + + def __init__(self, limit=1000, *args, **kwargs): + super(TotalTrendErrorAlarm, self).__init__(*args, **kwargs) + self.limit = limit + + def checkAlarm(self, trend): + if sum(trend.trendedValues) < self.limit: + return False + + self.announceAlarm(self.formatMessage(trend)) + return True From 01238a21855c01ec132ed02afaccc5584cb4c74e Mon Sep 17 00:00:00 2001 From: ostro Date: Sun, 4 Nov 2018 22:52:49 +0100 Subject: [PATCH 02/38] hashbang, basic docstring and authorship information --- overwatch/processing/trending/info.py | 5 +++++ overwatch/processing/trending/manager.py | 9 +++++++++ overwatch/processing/trending/objects/maximum.py | 5 +++++ overwatch/processing/trending/objects/mean.py | 5 +++++ overwatch/processing/trending/objects/object.py | 8 ++++++++ overwatch/processing/trending/objects/stdDev.py | 7 +++++++ tests/processing/trending/test_info.py | 5 +++++ tests/processing/trending/test_object.py | 5 +++++ tests/processing/trending/test_objects.py | 5 +++++ tests/unit/fixtures/trendingFixtures.py | 13 ++++++++----- 10 files changed, 62 insertions(+), 5 deletions(-) diff --git a/overwatch/processing/trending/info.py b/overwatch/processing/trending/info.py index 6314c9a3..49e8efd2 100644 --- a/overwatch/processing/trending/info.py +++ b/overwatch/processing/trending/info.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +""" Simple container of parameter for TrendingObject. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" try: from typing import * # noqa except ImportError: diff --git a/overwatch/processing/trending/manager.py b/overwatch/processing/trending/manager.py index d95c876c..77234119 100644 --- a/overwatch/processing/trending/manager.py +++ b/overwatch/processing/trending/manager.py @@ -1,3 +1,12 @@ +#!/usr/bin/env python +""" Class for management of trends. + +Prepare trending part of database, create trending objects, +notify appropriate objects about new histograms, manage processing trending histograms. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +.. code-author: Artur Wolak <>, AGH University of Science and Technology +""" import logging import os from collections import defaultdict diff --git a/overwatch/processing/trending/objects/maximum.py b/overwatch/processing/trending/objects/maximum.py index 4801f1fe..2e2f31a3 100644 --- a/overwatch/processing/trending/objects/maximum.py +++ b/overwatch/processing/trending/objects/maximum.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +""" Example trending object with maximum values. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" import numpy as np import ROOT diff --git a/overwatch/processing/trending/objects/mean.py b/overwatch/processing/trending/objects/mean.py index 950d04cb..4ab6d7e8 100644 --- a/overwatch/processing/trending/objects/mean.py +++ b/overwatch/processing/trending/objects/mean.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +""" Example trending object with mean values. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" import numpy as np import ROOT diff --git a/overwatch/processing/trending/objects/object.py b/overwatch/processing/trending/objects/object.py index cf76b131..319f01bb 100644 --- a/overwatch/processing/trending/objects/object.py +++ b/overwatch/processing/trending/objects/object.py @@ -1,3 +1,11 @@ +#!/usr/bin/env python +""" Abstract class for all trendingObjects. + +Has abstract methods to implement, +can save create histogram to image file and json file. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" import logging import os diff --git a/overwatch/processing/trending/objects/stdDev.py b/overwatch/processing/trending/objects/stdDev.py index 6d337120..253bf977 100644 --- a/overwatch/processing/trending/objects/stdDev.py +++ b/overwatch/processing/trending/objects/stdDev.py @@ -1,3 +1,10 @@ +#!/usr/bin/env python +""" Example trending object with standard deviation. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +.. code-author: Artur Wolak <>, AGH University of Science and Technology +""" + import numpy as np import ROOT diff --git a/tests/processing/trending/test_info.py b/tests/processing/trending/test_info.py index 4d7a223c..5ad50bcc 100644 --- a/tests/processing/trending/test_info.py +++ b/tests/processing/trending/test_info.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +""" Tests for TrendingInfo. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" import pytest from overwatch.processing.trending.info import TrendingInfoException, TrendingInfo diff --git a/tests/processing/trending/test_object.py b/tests/processing/trending/test_object.py index f3372afb..83980193 100644 --- a/tests/processing/trending/test_object.py +++ b/tests/processing/trending/test_object.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +""" Tests for TrendingObject. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" import pytest import ROOT from overwatch.processing.trending.objects.object import TrendingObject diff --git a/tests/processing/trending/test_objects.py b/tests/processing/trending/test_objects.py index f8cc4157..bbb5c6a3 100644 --- a/tests/processing/trending/test_objects.py +++ b/tests/processing/trending/test_objects.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +""" Tests for example implementations of TrendingObject. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" import pytest import ROOT diff --git a/tests/unit/fixtures/trendingFixtures.py b/tests/unit/fixtures/trendingFixtures.py index 6d8b832c..c98a3aab 100644 --- a/tests/unit/fixtures/trendingFixtures.py +++ b/tests/unit/fixtures/trendingFixtures.py @@ -1,13 +1,16 @@ +#!/usr/bin/env python +""" Fixtures for trending. + +All fixtures for trending have 'tf_' prefix. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" import ROOT import pytest from overwatch.processing.trending.constants import EXTENSION, ENTRIES from overwatch.processing.trending.objects.mean import MeanTrending -""" -All fixtures for trending have 'tf_' prefix -""" - @pytest.fixture def tf_trendingArgs(): @@ -22,7 +25,7 @@ def tf_infoArgs(): yield ["name", "desc", ["hist1", "hist2"], MeanTrending] -class Histogram: +class Histogram(object): functionNames = [ 'GetMaximum', 'GetMean', From a4482c38847f6c58374d446319ae1cbc06fc14d4 Mon Sep 17 00:00:00 2001 From: Nabywaniec Date: Sun, 25 Nov 2018 20:23:56 +0100 Subject: [PATCH 03/38] Create increasingValueAlarm.py --- .../processing/alarms/increasingValueAlarm.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 overwatch/processing/alarms/increasingValueAlarm.py diff --git a/overwatch/processing/alarms/increasingValueAlarm.py b/overwatch/processing/alarms/increasingValueAlarm.py new file mode 100644 index 00000000..05bc7318 --- /dev/null +++ b/overwatch/processing/alarms/increasingValueAlarm.py @@ -0,0 +1,19 @@ +from .alarm import Alarm + + +class IncreasingValueAlarm(Alarm): + def __init__(self, minVal=0, maxVal=100, *args, **kwargs): + super(IncreasingValueAlarm, self).__init__(*args, **kwargs) + self.minVal = minVal + self.maxVal = maxVal + self.ratio = 2.0 + + def checkAlarm(self, trend): + prevValue = trend.trendedValues[-2] + testedValue = trend.trendedValues[-1] + if testedValue <= self.ratio * prevValue: + return False + + alarm = "value: {}, prev: {}, increase more than: {}".format(testedValue, prevValue, self.ratio) + self.announceAlarm(self.formatMessage(trend, alarm)) + return True From cfc5bb9542a8267ea105cb40afef2cd46bde5b26 Mon Sep 17 00:00:00 2001 From: Nabywaniec Date: Sun, 25 Nov 2018 20:42:51 +0100 Subject: [PATCH 04/38] Create checkLastNValuesAlarm --- .../processing/alarms/checkLastNValuesAlarm | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 overwatch/processing/alarms/checkLastNValuesAlarm diff --git a/overwatch/processing/alarms/checkLastNValuesAlarm b/overwatch/processing/alarms/checkLastNValuesAlarm new file mode 100644 index 00000000..aca9a1ab --- /dev/null +++ b/overwatch/processing/alarms/checkLastNValuesAlarm @@ -0,0 +1,22 @@ +from .alarm import Alarm +import numpy as np + + +class IncreasingValueAlarm(Alarm): + def __init__(self, minVal=0, maxVal=100, *args, **kwargs): + super(IncreasingValueAlarm, self).__init__(*args, **kwargs) + self.minVal = minVal + self.maxVal = maxVal + self.N = 5 + + def checkAlarm(self, trend): + trendingValues = trend[-self.N:] + if (len(trendingValues) < self.N): + return False + mean = np.mean(trendingValues) + if (self.minVal < np.mean(mean) < self.maxVal): + return False + + alarm = "value of last: {} values not in {} {}".format(mean, self.minVal, self.maxVal) + self.announceAlarm(self.formatMessage(trend, alarm)) + return True From b3cc3d1adb267751684c35d3b5faf70238949ba6 Mon Sep 17 00:00:00 2001 From: Nabywaniec Date: Sun, 25 Nov 2018 20:56:33 +0100 Subject: [PATCH 05/38] Add files via upload --- .../processing/alarms/checkLastNAlarms.py | 24 +++++++++++++++++++ .../processing/alarms/decreasingValueAlarm.py | 19 +++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 overwatch/processing/alarms/checkLastNAlarms.py create mode 100644 overwatch/processing/alarms/decreasingValueAlarm.py diff --git a/overwatch/processing/alarms/checkLastNAlarms.py b/overwatch/processing/alarms/checkLastNAlarms.py new file mode 100644 index 00000000..891cf7be --- /dev/null +++ b/overwatch/processing/alarms/checkLastNAlarms.py @@ -0,0 +1,24 @@ +from .alarm import Alarm +import numpy as np + + +class IncreasingValueAlarm(Alarm): + def __init__(self, minVal=0, maxVal=100, *args, **kwargs): + super(IncreasingValueAlarm, self).__init__(*args, **kwargs) + self.minVal = minVal + self.maxVal = maxVal + self.ratio = 0.6 + self.N = 5 + + def checkAlarm(self, trend): + trendingValues = trend[-self.N:] + if (len(trendingValues) < self.N): + return False + notInBorderValues = [trendValue for trendValue in trendingValues if self.maxVal > trendingValues > self.minVal] + if (len(notInBorderValues) > (1 - self.ratio) * self.N): + return False + + alarm = "more than {} % values of last: {} values not in {} {}".format(self.ratio * 10, self.N, self.minVal, + self.maxVal) + self.announceAlarm(self.formatMessage(trend, alarm)) + return True diff --git a/overwatch/processing/alarms/decreasingValueAlarm.py b/overwatch/processing/alarms/decreasingValueAlarm.py new file mode 100644 index 00000000..d839d7ca --- /dev/null +++ b/overwatch/processing/alarms/decreasingValueAlarm.py @@ -0,0 +1,19 @@ +from .alarm import Alarm + + +class IncreasingValueAlarm(Alarm): + def __init__(self, minVal=0, maxVal=100, *args, **kwargs): + super(IncreasingValueAlarm, self).__init__(*args, **kwargs) + self.minVal = minVal + self.maxVal = maxVal + self.ratio = 2.0 + + def checkAlarm(self, trend): + prevValue = trend.trendedValues[-2] + testedValue = trend.trendedValues[-1] + if prevValue <= self.ratio * testedValue: + return False + + alarm = "value: {}, prev: {}, decrease more than: {}".format(testedValue, prevValue, self.ratio) + self.announceAlarm(self.formatMessage(trend, alarm)) + return True From 6e00e041af402ba91985a14536faa573a097db9b Mon Sep 17 00:00:00 2001 From: ostro Date: Sun, 25 Nov 2018 21:15:10 +0100 Subject: [PATCH 06/38] change interface alarms --- overwatch/processing/alarms/alarm.py | 9 +++------ overwatch/processing/alarms/andAlarm.py | 5 ++--- overwatch/processing/alarms/boarderAlarm.py | 6 +++--- .../processing/alarms/checkLastNAlarms.py | 7 +++---- ...tNValuesAlarm => checkLastNValuesAlarm.py} | 6 +++--- .../processing/alarms/decreasingValueAlarm.py | 2 +- .../processing/alarms/increasingValueAlarm.py | 2 +- overwatch/processing/alarms/orAlarm.py | 19 +++++++++++++++++++ overwatch/processing/alarms/test_alarms.py | 6 ++---- .../processing/alarms/totalTrendErrorAlarm.py | 15 --------------- overwatch/processing/detectors/TPC.py | 3 ++- overwatch/processing/trending/info.py | 13 +++++++++++-- overwatch/processing/trending/manager.py | 2 ++ .../processing/trending/objects/object.py | 5 +++++ 14 files changed, 57 insertions(+), 43 deletions(-) rename overwatch/processing/alarms/{checkLastNValuesAlarm => checkLastNValuesAlarm.py} (78%) create mode 100644 overwatch/processing/alarms/orAlarm.py delete mode 100644 overwatch/processing/alarms/totalTrendErrorAlarm.py diff --git a/overwatch/processing/alarms/alarm.py b/overwatch/processing/alarms/alarm.py index 9abe2252..a2bdc38e 100644 --- a/overwatch/processing/alarms/alarm.py +++ b/overwatch/processing/alarms/alarm.py @@ -7,7 +7,7 @@ class Alarm: - def __init__(self, *args, alarmText='', **kwargs): + def __init__(self, alarmText=''): self.alarmText = alarmText self.receivers = [] @@ -18,10 +18,7 @@ def checkAlarm(self, trend): # type: (TrendingObject) -> bool """abstract method""" raise NotImplementedError - def announceAlarm(self, msg): + def _announceAlarm(self, msg): # type: (str) -> None + msg = "[{alarmText}]: {msg}".format(alarmText=self.alarmText, msg=msg) for receiver in self.receivers: receiver(msg) - - def formatMessage(self, trend='', msg=''): # type: (TrendingObject) -> str - return "{alarmText} {trend} {msg}".format( - alarmText=self.alarmText, trend=trend, msg=msg) diff --git a/overwatch/processing/alarms/andAlarm.py b/overwatch/processing/alarms/andAlarm.py index e5981d10..bb52daef 100644 --- a/overwatch/processing/alarms/andAlarm.py +++ b/overwatch/processing/alarms/andAlarm.py @@ -7,8 +7,8 @@ class AndAlarm(Alarm): - def __init__(self, children=None, *args, **kwargs): # type: (List[Alarm]) -> None - super(AndAlarm, self).__init__(*args, **kwargs) + def __init__(self, *children, alarmText=''): # type: (*Alarm) -> None + super(AndAlarm, self).__init__(alarmText=alarmText) self.children = [] if not children else children def checkAlarm(self, trend): @@ -16,5 +16,4 @@ def checkAlarm(self, trend): if not child.checkAlarm(trend): return False - self.announceAlarm(self.formatMessage(trend)) return True diff --git a/overwatch/processing/alarms/boarderAlarm.py b/overwatch/processing/alarms/boarderAlarm.py index a423d14d..e1ceb796 100644 --- a/overwatch/processing/alarms/boarderAlarm.py +++ b/overwatch/processing/alarms/boarderAlarm.py @@ -2,8 +2,8 @@ class BorderAlarm(Alarm): - def __init__(self, minVal=0, maxVal=100, *args, **kwargs): - super(BorderAlarm, self).__init__(*args, **kwargs) + def __init__(self, minVal=0, maxVal=100, alarmText=''): + super(BorderAlarm, self).__init__(alarmText=alarmText) self.minVal = minVal self.maxVal = maxVal @@ -13,5 +13,5 @@ def checkAlarm(self, trend): return False alarm = "value: {} not in [{}, {}]".format(testedValue, self.minVal, self.maxVal) - self.announceAlarm(self.formatMessage(trend, alarm)) + self._announceAlarm(alarm) return True diff --git a/overwatch/processing/alarms/checkLastNAlarms.py b/overwatch/processing/alarms/checkLastNAlarms.py index 891cf7be..f6113dc9 100644 --- a/overwatch/processing/alarms/checkLastNAlarms.py +++ b/overwatch/processing/alarms/checkLastNAlarms.py @@ -1,5 +1,4 @@ from .alarm import Alarm -import numpy as np class IncreasingValueAlarm(Alarm): @@ -12,13 +11,13 @@ def __init__(self, minVal=0, maxVal=100, *args, **kwargs): def checkAlarm(self, trend): trendingValues = trend[-self.N:] - if (len(trendingValues) < self.N): + if len(trendingValues) < self.N: return False notInBorderValues = [trendValue for trendValue in trendingValues if self.maxVal > trendingValues > self.minVal] - if (len(notInBorderValues) > (1 - self.ratio) * self.N): + if len(notInBorderValues) > (1 - self.ratio) * self.N: return False alarm = "more than {} % values of last: {} values not in {} {}".format(self.ratio * 10, self.N, self.minVal, self.maxVal) - self.announceAlarm(self.formatMessage(trend, alarm)) + self._announceAlarm(alarm) return True diff --git a/overwatch/processing/alarms/checkLastNValuesAlarm b/overwatch/processing/alarms/checkLastNValuesAlarm.py similarity index 78% rename from overwatch/processing/alarms/checkLastNValuesAlarm rename to overwatch/processing/alarms/checkLastNValuesAlarm.py index aca9a1ab..38adee45 100644 --- a/overwatch/processing/alarms/checkLastNValuesAlarm +++ b/overwatch/processing/alarms/checkLastNValuesAlarm.py @@ -11,12 +11,12 @@ def __init__(self, minVal=0, maxVal=100, *args, **kwargs): def checkAlarm(self, trend): trendingValues = trend[-self.N:] - if (len(trendingValues) < self.N): + if len(trendingValues) < self.N: return False mean = np.mean(trendingValues) - if (self.minVal < np.mean(mean) < self.maxVal): + if self.minVal < np.mean(mean) < self.maxVal: return False alarm = "value of last: {} values not in {} {}".format(mean, self.minVal, self.maxVal) - self.announceAlarm(self.formatMessage(trend, alarm)) + self._announceAlarm(alarm) return True diff --git a/overwatch/processing/alarms/decreasingValueAlarm.py b/overwatch/processing/alarms/decreasingValueAlarm.py index d839d7ca..7777b5d1 100644 --- a/overwatch/processing/alarms/decreasingValueAlarm.py +++ b/overwatch/processing/alarms/decreasingValueAlarm.py @@ -15,5 +15,5 @@ def checkAlarm(self, trend): return False alarm = "value: {}, prev: {}, decrease more than: {}".format(testedValue, prevValue, self.ratio) - self.announceAlarm(self.formatMessage(trend, alarm)) + self._announceAlarm(alarm) return True diff --git a/overwatch/processing/alarms/increasingValueAlarm.py b/overwatch/processing/alarms/increasingValueAlarm.py index 05bc7318..8e366487 100644 --- a/overwatch/processing/alarms/increasingValueAlarm.py +++ b/overwatch/processing/alarms/increasingValueAlarm.py @@ -15,5 +15,5 @@ def checkAlarm(self, trend): return False alarm = "value: {}, prev: {}, increase more than: {}".format(testedValue, prevValue, self.ratio) - self.announceAlarm(self.formatMessage(trend, alarm)) + self._announceAlarm(alarm) return True diff --git a/overwatch/processing/alarms/orAlarm.py b/overwatch/processing/alarms/orAlarm.py new file mode 100644 index 00000000..b2e002c3 --- /dev/null +++ b/overwatch/processing/alarms/orAlarm.py @@ -0,0 +1,19 @@ +from .alarm import Alarm + +try: + from typing import * # noqa +except ImportError: + pass + + +class OrAlarm(Alarm): + def __init__(self, *children, alarmText=''): # type: (*Alarm) -> None + super(OrAlarm, self).__init__(alarmText=alarmText) + self.children = [] if not children else children + + def checkAlarm(self, trend): + for child in self.children: + if child.checkAlarm(trend): + return True + + return False diff --git a/overwatch/processing/alarms/test_alarms.py b/overwatch/processing/alarms/test_alarms.py index 3e330293..3bec2d65 100644 --- a/overwatch/processing/alarms/test_alarms.py +++ b/overwatch/processing/alarms/test_alarms.py @@ -1,7 +1,6 @@ from overwatch.processing.alarms.collectors import workerMail, printCollector, httpCollector, MailSender from overwatch.processing.alarms.andAlarm import AndAlarm from overwatch.processing.alarms.boarderAlarm import BorderAlarm -from overwatch.processing.alarms.totalTrendErrorAlarm import TotalTrendErrorAlarm class TrendingObjectMock: @@ -29,8 +28,7 @@ def alarmConfig(): borderError.receivers = [workerMail, httpCollector] borderAlarm = BorderAlarm(maxVal=90) - totalAlarm = TotalTrendErrorAlarm(limit=1000) - seriousAlarm = AndAlarm([borderAlarm, totalAlarm], alarmText="Serious Alarm") + seriousAlarm = AndAlarm(borderAlarm, alarmText="Serious Alarm") cernBoss = MailSender("boss@cern") seriousAlarm.addReceiver(cernBoss) @@ -40,7 +38,7 @@ def alarmConfig(): def main(): to = TrendingObjectMock(alarmConfig()) - values = [3, 14, 60, 80, 8888, 7, 95] + values = [3, 14, 15, 92, 65, 35, 89, 79] for i, val in enumerate(values): print("\nVal number: {i} New value:{val}".format(i=i, val=val)) to.addNewValue(val) diff --git a/overwatch/processing/alarms/totalTrendErrorAlarm.py b/overwatch/processing/alarms/totalTrendErrorAlarm.py deleted file mode 100644 index 524dc805..00000000 --- a/overwatch/processing/alarms/totalTrendErrorAlarm.py +++ /dev/null @@ -1,15 +0,0 @@ -from .alarm import Alarm - - -class TotalTrendErrorAlarm(Alarm): - - def __init__(self, limit=1000, *args, **kwargs): - super(TotalTrendErrorAlarm, self).__init__(*args, **kwargs) - self.limit = limit - - def checkAlarm(self, trend): - if sum(trend.trendedValues) < self.limit: - return False - - self.announceAlarm(self.formatMessage(trend)) - return True diff --git a/overwatch/processing/detectors/TPC.py b/overwatch/processing/detectors/TPC.py index 1d18ae39..435b27ec 100644 --- a/overwatch/processing/detectors/TPC.py +++ b/overwatch/processing/detectors/TPC.py @@ -66,7 +66,8 @@ def getTrendingObjectInfo(): # type: () -> List[TrendingInfo] trendingInfo = [] for prefix, cls in trendingNameToObject.items(): for name, desc, histograms in infoList: - trendingInfo.append(TrendingInfo(prefix + name, prefix + desc, histograms, cls)) + ti = TrendingInfo(prefix + name, prefix + desc, histograms, cls) + trendingInfo.append(ti) return trendingInfo diff --git a/overwatch/processing/trending/info.py b/overwatch/processing/trending/info.py index 6314c9a3..fd071f39 100644 --- a/overwatch/processing/trending/info.py +++ b/overwatch/processing/trending/info.py @@ -1,5 +1,7 @@ + try: from typing import * # noqa + from overwatch.processing.alarms.alarm import Alarm # noqa except ImportError: pass @@ -23,7 +25,7 @@ class TrendingInfo: When TrendingInfo is initialized, data are validated. """ - __slots__ = ['name', 'desc', 'histogramNames', 'trendingClass'] + __slots__ = ['name', 'desc', 'histogramNames', 'trendingClass', '_alarms'] def __init__(self, name, desc, histogramNames, trendingClass): """ @@ -40,12 +42,19 @@ def __init__(self, name, desc, histogramNames, trendingClass): self.histogramNames = self._validateHist(histogramNames) self.trendingClass = self._validateTrendingClass(trendingClass) + self._alarms = [] + + def addAlarm(self, alarm): # type: (Alarm) -> None + self._alarms.append(alarm) + def createTrendingClass(self, subsystemName, parameters): # type: (str, dict) -> TrendingObject """Create instance of TrendingObject from previously set parameters Returns: TrendingObject: newly created object """ - return self.trendingClass(self.name, self.desc, self.histogramNames, subsystemName, parameters) + trend = self.trendingClass(self.name, self.desc, self.histogramNames, subsystemName, parameters) + trend.setAlarms(self._alarms) + return trend @staticmethod def _validate(obj): # type: (str) -> str diff --git a/overwatch/processing/trending/manager.py b/overwatch/processing/trending/manager.py index d95c876c..9168aa7e 100644 --- a/overwatch/processing/trending/manager.py +++ b/overwatch/processing/trending/manager.py @@ -107,3 +107,5 @@ def notifyAboutNewHistogramValue(self, hist): # type: (histogramContainer) -> N """ADD DOC""" for trend in self.histToTrending.get(hist.histName, []): trend.extractTrendValue(hist) + for alarm in trend.alarms: + alarm.checkAlarm(trend) diff --git a/overwatch/processing/trending/objects/object.py b/overwatch/processing/trending/objects/object.py index cf76b131..67a19377 100644 --- a/overwatch/processing/trending/objects/object.py +++ b/overwatch/processing/trending/objects/object.py @@ -29,6 +29,7 @@ def __init__(self, name, description, histogramNames, subsystemName, parameters) self.currentEntry = 0 self.maxEntries = self.parameters.get(CON.ENTRIES, 100) self.trendedValues = self.initializeTrendingArray() + self.alarms = [] self.histogram = None # Ensure that the axis and points are drawn on the TGraph @@ -100,3 +101,7 @@ def resetCanvas(canvas): canvas.SetLogx(False) canvas.SetLogy(False) canvas.SetLogz(False) + + def addAlarms(self, alarms): # type: (List['Alarm']) -> None + """ ADD DOC""" + self.alarms.extend(alarms) From 6f53a69e52a29038da01655d4a133474569c7673 Mon Sep 17 00:00:00 2001 From: ostro Date: Sun, 25 Nov 2018 21:35:01 +0100 Subject: [PATCH 07/38] python2 fix --- overwatch/processing/alarms/andAlarm.py | 2 +- overwatch/processing/alarms/orAlarm.py | 2 +- overwatch/processing/alarms/test_alarms.py | 2 +- tests/processing/alarms/__init__.py | 0 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 tests/processing/alarms/__init__.py diff --git a/overwatch/processing/alarms/andAlarm.py b/overwatch/processing/alarms/andAlarm.py index bb52daef..fda45664 100644 --- a/overwatch/processing/alarms/andAlarm.py +++ b/overwatch/processing/alarms/andAlarm.py @@ -7,7 +7,7 @@ class AndAlarm(Alarm): - def __init__(self, *children, alarmText=''): # type: (*Alarm) -> None + def __init__(self, alarmText='', *children): # type: (str, *Alarm) -> None super(AndAlarm, self).__init__(alarmText=alarmText) self.children = [] if not children else children diff --git a/overwatch/processing/alarms/orAlarm.py b/overwatch/processing/alarms/orAlarm.py index b2e002c3..a689ebad 100644 --- a/overwatch/processing/alarms/orAlarm.py +++ b/overwatch/processing/alarms/orAlarm.py @@ -7,7 +7,7 @@ class OrAlarm(Alarm): - def __init__(self, *children, alarmText=''): # type: (*Alarm) -> None + def __init__(self, alarmText='', *children): # type: (str, *Alarm) -> None super(OrAlarm, self).__init__(alarmText=alarmText) self.children = [] if not children else children diff --git a/overwatch/processing/alarms/test_alarms.py b/overwatch/processing/alarms/test_alarms.py index 3bec2d65..13688ae5 100644 --- a/overwatch/processing/alarms/test_alarms.py +++ b/overwatch/processing/alarms/test_alarms.py @@ -28,7 +28,7 @@ def alarmConfig(): borderError.receivers = [workerMail, httpCollector] borderAlarm = BorderAlarm(maxVal=90) - seriousAlarm = AndAlarm(borderAlarm, alarmText="Serious Alarm") + seriousAlarm = AndAlarm("Serious Alarm", borderAlarm) cernBoss = MailSender("boss@cern") seriousAlarm.addReceiver(cernBoss) diff --git a/tests/processing/alarms/__init__.py b/tests/processing/alarms/__init__.py new file mode 100644 index 00000000..e69de29b From 174e2036ed558da325ee2c6abcae3ccd6720fd19 Mon Sep 17 00:00:00 2001 From: Nabywaniec Date: Tue, 27 Nov 2018 18:30:00 +0100 Subject: [PATCH 08/38] Update checkLastNAlarms.py --- .../processing/alarms/checkLastNAlarms.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/overwatch/processing/alarms/checkLastNAlarms.py b/overwatch/processing/alarms/checkLastNAlarms.py index f6113dc9..59f9d257 100644 --- a/overwatch/processing/alarms/checkLastNAlarms.py +++ b/overwatch/processing/alarms/checkLastNAlarms.py @@ -1,23 +1,23 @@ from .alarm import Alarm -class IncreasingValueAlarm(Alarm): - def __init__(self, minVal=0, maxVal=100, *args, **kwargs): +class checkLastNAlarms(Alarm): + def __init__(self, minVal=0, maxVal=100, ratio=0.6, N=5, *args, **kwargs): super(IncreasingValueAlarm, self).__init__(*args, **kwargs) self.minVal = minVal self.maxVal = maxVal - self.ratio = 0.6 - self.N = 5 + self.ratio = ratio + self.N = N def checkAlarm(self, trend): - trendingValues = trend[-self.N:] - if len(trendingValues) < self.N: + if len(trend.trendedValues) < self.N: return False - notInBorderValues = [trendValue for trendValue in trendingValues if self.maxVal > trendingValues > self.minVal] - if len(notInBorderValues) > (1 - self.ratio) * self.N: + trendedValues = trend[-self.N:] + inBorderValues = [trendedValue for trendedValue in trendedValues if self.maxVal > trendedValue > self.minVal] + if len(inBorderValues) >= self.ratio * self.N: return False - alarm = "more than {} % values of last: {} values not in {} {}".format(self.ratio * 10, self.N, self.minVal, + alarm = "less than {} % values of last: {} values not in {} {}".format(self.ratio * 10, self.N, self.minVal, self.maxVal) self._announceAlarm(alarm) return True From 7cd7c89e88cc3b276ae76d08d41b82f8c79cc966 Mon Sep 17 00:00:00 2001 From: Nabywaniec Date: Tue, 27 Nov 2018 18:31:34 +0100 Subject: [PATCH 09/38] Update and rename checkLastNAlarms.py to checkLastNAlarm.py --- .../alarms/{checkLastNAlarms.py => checkLastNAlarm.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename overwatch/processing/alarms/{checkLastNAlarms.py => checkLastNAlarm.py} (87%) diff --git a/overwatch/processing/alarms/checkLastNAlarms.py b/overwatch/processing/alarms/checkLastNAlarm.py similarity index 87% rename from overwatch/processing/alarms/checkLastNAlarms.py rename to overwatch/processing/alarms/checkLastNAlarm.py index 59f9d257..773704a2 100644 --- a/overwatch/processing/alarms/checkLastNAlarms.py +++ b/overwatch/processing/alarms/checkLastNAlarm.py @@ -1,9 +1,9 @@ from .alarm import Alarm -class checkLastNAlarms(Alarm): +class checkLastNAlarm(Alarm): def __init__(self, minVal=0, maxVal=100, ratio=0.6, N=5, *args, **kwargs): - super(IncreasingValueAlarm, self).__init__(*args, **kwargs) + super(checkLastNAlarm, self).__init__(*args, **kwargs) self.minVal = minVal self.maxVal = maxVal self.ratio = ratio From b9af95d8a9fb3fd5132f55f94dd12b2826626c55 Mon Sep 17 00:00:00 2001 From: Nabywaniec Date: Tue, 27 Nov 2018 18:34:06 +0100 Subject: [PATCH 10/38] Update checkLastNValuesAlarm.py --- .../processing/alarms/checkLastNValuesAlarm.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/overwatch/processing/alarms/checkLastNValuesAlarm.py b/overwatch/processing/alarms/checkLastNValuesAlarm.py index 38adee45..a2f55f66 100644 --- a/overwatch/processing/alarms/checkLastNValuesAlarm.py +++ b/overwatch/processing/alarms/checkLastNValuesAlarm.py @@ -2,21 +2,21 @@ import numpy as np -class IncreasingValueAlarm(Alarm): - def __init__(self, minVal=0, maxVal=100, *args, **kwargs): - super(IncreasingValueAlarm, self).__init__(*args, **kwargs) +class checkLastNValuesAlarm(Alarm): + def __init__(self, minVal=0, maxVal=100, N=5, *args, **kwargs): + super(checkLastNValuesAlarm, self).__init__(*args, **kwargs) self.minVal = minVal self.maxVal = maxVal self.N = 5 def checkAlarm(self, trend): - trendingValues = trend[-self.N:] - if len(trendingValues) < self.N: + if len(trend.trendedValues) < self.N: return False - mean = np.mean(trendingValues) + trendedValues = np.array(trend.trendedValues) + mean = np.mean(trendedValues) if self.minVal < np.mean(mean) < self.maxVal: return False - alarm = "value of last: {} values not in {} {}".format(mean, self.minVal, self.maxVal) + alarm = "mean value of last: {} values not in {} {}".format(self.N, self.minVal, self.maxVal) self._announceAlarm(alarm) return True From a7b3f527cd73135573e205b8cc103d41f8ac04d9 Mon Sep 17 00:00:00 2001 From: Nabywaniec Date: Tue, 27 Nov 2018 18:34:34 +0100 Subject: [PATCH 11/38] Delete decreasingValueAlarm.py --- .../processing/alarms/decreasingValueAlarm.py | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 overwatch/processing/alarms/decreasingValueAlarm.py diff --git a/overwatch/processing/alarms/decreasingValueAlarm.py b/overwatch/processing/alarms/decreasingValueAlarm.py deleted file mode 100644 index 7777b5d1..00000000 --- a/overwatch/processing/alarms/decreasingValueAlarm.py +++ /dev/null @@ -1,19 +0,0 @@ -from .alarm import Alarm - - -class IncreasingValueAlarm(Alarm): - def __init__(self, minVal=0, maxVal=100, *args, **kwargs): - super(IncreasingValueAlarm, self).__init__(*args, **kwargs) - self.minVal = minVal - self.maxVal = maxVal - self.ratio = 2.0 - - def checkAlarm(self, trend): - prevValue = trend.trendedValues[-2] - testedValue = trend.trendedValues[-1] - if prevValue <= self.ratio * testedValue: - return False - - alarm = "value: {}, prev: {}, decrease more than: {}".format(testedValue, prevValue, self.ratio) - self._announceAlarm(alarm) - return True From 6329965d17056be7c2f7124dc01d543103944fe4 Mon Sep 17 00:00:00 2001 From: Nabywaniec Date: Tue, 27 Nov 2018 18:44:45 +0100 Subject: [PATCH 12/38] Update and rename increasingValueAlarm.py to changingValueAlarm.py --- .../processing/alarms/changingValueAlarm.py | 26 +++++++++++++++++++ .../processing/alarms/increasingValueAlarm.py | 19 -------------- 2 files changed, 26 insertions(+), 19 deletions(-) create mode 100644 overwatch/processing/alarms/changingValueAlarm.py delete mode 100644 overwatch/processing/alarms/increasingValueAlarm.py diff --git a/overwatch/processing/alarms/changingValueAlarm.py b/overwatch/processing/alarms/changingValueAlarm.py new file mode 100644 index 00000000..3caf7b73 --- /dev/null +++ b/overwatch/processing/alarms/changingValueAlarm.py @@ -0,0 +1,26 @@ +from .alarm import Alarm + + +class ChangingValueAlarm(Alarm): + def __init__(self, minVal=0, maxVal=100, ratio=2.0, * args, **kwargs): + super(ChangingValueAlarm, self).__init__(*args, **kwargs) + self.minVal = minVal + self.maxVal = maxVal + self.ratio = 2.0 + + def checkAlarm(self, trend): + if (len(trend.trendedValues < 2)): + return False + prevValue = trend.trendedValues[-2] + testedValue = trend.trendedValues[-1] + + if testedValue > 0 and prevValue > 0 and (testedValue <= self.ratio * prevValue + or prevValue * self.ratio <= testedValue): + return False + if testedValue < 0 and prevValue < 0 and (abs(prevValue) <= self.ratio * abs(testedValue) + or abs(testedValue) * self.ratio <= abs(prevValue)): + return False + + alarm = "value: {}, prev: {}, change more than: {}".format(testedValue, prevValue, self.ratio) + self._announceAlarm(alarm) + return True diff --git a/overwatch/processing/alarms/increasingValueAlarm.py b/overwatch/processing/alarms/increasingValueAlarm.py deleted file mode 100644 index 8e366487..00000000 --- a/overwatch/processing/alarms/increasingValueAlarm.py +++ /dev/null @@ -1,19 +0,0 @@ -from .alarm import Alarm - - -class IncreasingValueAlarm(Alarm): - def __init__(self, minVal=0, maxVal=100, *args, **kwargs): - super(IncreasingValueAlarm, self).__init__(*args, **kwargs) - self.minVal = minVal - self.maxVal = maxVal - self.ratio = 2.0 - - def checkAlarm(self, trend): - prevValue = trend.trendedValues[-2] - testedValue = trend.trendedValues[-1] - if testedValue <= self.ratio * prevValue: - return False - - alarm = "value: {}, prev: {}, increase more than: {}".format(testedValue, prevValue, self.ratio) - self._announceAlarm(alarm) - return True From c97e1a923a8251b53dcd5e58187ba9cb5812ce80 Mon Sep 17 00:00:00 2001 From: arturro96 Date: Tue, 27 Nov 2018 20:36:14 +0100 Subject: [PATCH 13/38] Added notifications --- overwatch/processing/alarms/alarm.py | 2 +- overwatch/processing/alarms/collectors.py | 64 +++++++++++++++++++++- overwatch/processing/alarms/test_alarms.py | 13 ++++- 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/overwatch/processing/alarms/alarm.py b/overwatch/processing/alarms/alarm.py index a2bdc38e..df3afd51 100644 --- a/overwatch/processing/alarms/alarm.py +++ b/overwatch/processing/alarms/alarm.py @@ -6,7 +6,7 @@ from ..trending.objects.object import TrendingObject # noqa -class Alarm: +class Alarm(object): def __init__(self, alarmText=''): self.alarmText = alarmText self.receivers = [] diff --git a/overwatch/processing/alarms/collectors.py b/overwatch/processing/alarms/collectors.py index 606d7674..b8bee792 100644 --- a/overwatch/processing/alarms/collectors.py +++ b/overwatch/processing/alarms/collectors.py @@ -1,3 +1,43 @@ +from slackclient import SlackClient +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +# from overwatch.base import config +# (alarmsParameters, filesRead) = config.readConfig(config.configurationType.alarms) +import yaml + +with open("config.yaml", 'r') as ymlfile: + alarmsParameters = yaml.load(ymlfile) + +# works in Python 2 & 3 +class _Singleton(type): + """ A metaclass that creates a Singleton base class when called. """ + _instances = {} + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + + +class Singleton(_Singleton('SingletonMeta', (object,), {})): + # https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python + pass + + +class Mail(Singleton): + def __init__(self): + smtpSettings = alarmsParameters["email_delivery"]["smtp_settings"] + host = smtpSettings["address"] + port = smtpSettings["port"] + password = smtpSettings["password"] + self.user_name = smtpSettings["user_name"] + self.s = smtplib.SMTP(host=host, port=port) + self._login(password) + + def _login(self, password): + self.s.starttls() + self.s.login(user=self.user_name, password=password) def printCollector(alarm): @@ -7,16 +47,38 @@ def printCollector(alarm): class MailSender: def __init__(self, address): self.address = address + self.mail = Mail() def __call__(self, alarm): self.sendMail(alarm) def sendMail(self, payload): + msg = MIMEMultipart() + msg['From'] = self.mail.user_name + msg['To'] = self.address + msg['Subject'] = 'Test Alarm' + msg.attach(MIMEText(payload, 'plain')) printCollector("MAIL TO:{} FROM:alarm@overwatch PAYLOAD:'{}'".format(self.address, payload)) + self.mail.s.sendmail(self.mail.user_name, self.address, msg.as_string()) + + +class SlackNotification(Singleton): + def __init__(self): + self.sc = SlackClient(alarmsParameters["apiToken"]) + self.channel = alarmsParameters["slackChannel"] + + def __call__(self, alarm): + self.sendMessage(alarm) + + def sendMessage(self, payload): + self.sc.api_call('chat.postMessage', channel=self.channel, + text=payload, username='Alarms OVERWATCH', + icon_emoji=':robot_face:') def httpCollector(alarm): printCollector("HTTP: {}".format(alarm)) -workerMail = MailSender("worker@cern") +workerMail = MailSender("test@mail") +workerSlack = SlackNotification() diff --git a/overwatch/processing/alarms/test_alarms.py b/overwatch/processing/alarms/test_alarms.py index 13688ae5..c468782a 100644 --- a/overwatch/processing/alarms/test_alarms.py +++ b/overwatch/processing/alarms/test_alarms.py @@ -1,7 +1,13 @@ -from overwatch.processing.alarms.collectors import workerMail, printCollector, httpCollector, MailSender +from overwatch.processing.alarms.collectors import workerMail, workerSlack, printCollector, httpCollector, MailSender, \ + Mail from overwatch.processing.alarms.andAlarm import AndAlarm from overwatch.processing.alarms.boarderAlarm import BorderAlarm +import yaml + +with open("config.yaml", 'r') as ymlfile: + alarmsParameters = yaml.load(ymlfile) + class TrendingObjectMock: def __init__(self, alarms): @@ -25,17 +31,18 @@ def alarmConfig(): boarderWarning.addReceiver(printCollector) borderError = BorderAlarm(maxVal=70, alarmText="ERROR") - borderError.receivers = [workerMail, httpCollector] + borderError.receivers = [workerMail, httpCollector, workerSlack] borderAlarm = BorderAlarm(maxVal=90) seriousAlarm = AndAlarm("Serious Alarm", borderAlarm) - cernBoss = MailSender("boss@cern") + cernBoss = MailSender("test@mail") seriousAlarm.addReceiver(cernBoss) return [boarderWarning, borderError, seriousAlarm] def main(): + # Mail(alarmsParameters) to = TrendingObjectMock(alarmConfig()) values = [3, 14, 15, 92, 65, 35, 89, 79] From fc1c967a813d90a9cb85c9dd26b3b12dfc7bc7be Mon Sep 17 00:00:00 2001 From: arturro96 Date: Thu, 29 Nov 2018 11:31:06 +0100 Subject: [PATCH 14/38] Reading configuration update --- overwatch/processing/alarms/collectors.py | 44 +++++++++-------------- overwatch/processing/config.yaml | 20 +++++++++++ overwatch/processing/trending/manager.py | 4 +++ 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/overwatch/processing/alarms/collectors.py b/overwatch/processing/alarms/collectors.py index b8bee792..f37e1144 100644 --- a/overwatch/processing/alarms/collectors.py +++ b/overwatch/processing/alarms/collectors.py @@ -3,13 +3,6 @@ from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -# from overwatch.base import config -# (alarmsParameters, filesRead) = config.readConfig(config.configurationType.alarms) -import yaml - -with open("config.yaml", 'r') as ymlfile: - alarmsParameters = yaml.load(ymlfile) - # works in Python 2 & 3 class _Singleton(type): """ A metaclass that creates a Singleton base class when called. """ @@ -19,32 +12,30 @@ def __call__(cls, *args, **kwargs): cls._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] - class Singleton(_Singleton('SingletonMeta', (object,), {})): # https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python pass - class Mail(Singleton): - def __init__(self): - smtpSettings = alarmsParameters["email_delivery"]["smtp_settings"] - host = smtpSettings["address"] - port = smtpSettings["port"] - password = smtpSettings["password"] - self.user_name = smtpSettings["user_name"] - self.s = smtplib.SMTP(host=host, port=port) - self._login(password) + def __init__(self, alarmsParameters=None): + if alarmsParameters is not None: + smtpSettings = alarmsParameters["email_delivery"]["smtp_settings"] + host = smtpSettings["address"] + port = smtpSettings["port"] + password = smtpSettings["password"] + self.user_name = smtpSettings["user_name"] + self.s = smtplib.SMTP(host=host, port=port) + self._login(password) def _login(self, password): self.s.starttls() self.s.login(user=self.user_name, password=password) - def printCollector(alarm): print(alarm) - class MailSender: + """ADD DOC""" def __init__(self, address): self.address = address self.mail = Mail() @@ -60,12 +51,13 @@ def sendMail(self, payload): msg.attach(MIMEText(payload, 'plain')) printCollector("MAIL TO:{} FROM:alarm@overwatch PAYLOAD:'{}'".format(self.address, payload)) self.mail.s.sendmail(self.mail.user_name, self.address, msg.as_string()) - - + class SlackNotification(Singleton): - def __init__(self): - self.sc = SlackClient(alarmsParameters["apiToken"]) - self.channel = alarmsParameters["slackChannel"] + """ADD DOC""" + def __init__(self, alarmsParameters=None): + if alarmsParameters is not None: + self.sc = SlackClient(alarmsParameters["apiToken"]) + self.channel = alarmsParameters["slackChannel"] def __call__(self, alarm): self.sendMessage(alarm) @@ -75,10 +67,6 @@ def sendMessage(self, payload): text=payload, username='Alarms OVERWATCH', icon_emoji=':robot_face:') - def httpCollector(alarm): printCollector("HTTP: {}".format(alarm)) - -workerMail = MailSender("test@mail") -workerSlack = SlackNotification() diff --git a/overwatch/processing/config.yaml b/overwatch/processing/config.yaml index b3013c8b..baf58ffb 100644 --- a/overwatch/processing/config.yaml +++ b/overwatch/processing/config.yaml @@ -29,3 +29,23 @@ cumulativeMode: True # risk in changing it). dirPrefix: *dataFolder +# Email configuration +email_delivery: + delivery_method: :smtp + smtp_settings: + enable_starttls_auto: true + address: "smtp.gmail.com" + port: 587 + domain: "smtp.gmail.com" + authentication: :plain + user_name: "" + password: "" + +recipients: "" + +# Slack token +apiToken: '' + +# Slack channel +slackChannel: "" + diff --git a/overwatch/processing/trending/manager.py b/overwatch/processing/trending/manager.py index 9168aa7e..daea34b7 100644 --- a/overwatch/processing/trending/manager.py +++ b/overwatch/processing/trending/manager.py @@ -8,6 +8,7 @@ import overwatch.processing.pluginManager as pluginManager import overwatch.processing.trending.constants as CON +from overwatch.processing.alarms.collectors import Mail, SlackNotification logger = logging.getLogger(__name__) @@ -33,6 +34,8 @@ def __init__(self, dbRoot, parameters): # type: (PersistentMapping, dict)->None self.trendingDB = dbRoot[CON.TRENDING] # type: BTree[str, BTree[str, TrendingObject]] self._prepareDirStructure() + Mail(alarmsParameters=parameters) + SlackNotification(alarmsParameters=parameters) def _prepareDirStructure(self): trendingDir = os.path.join(self.parameters[CON.DIR_PREFIX], CON.TRENDING, '{{subsystemName}}', '{type}') @@ -109,3 +112,4 @@ def notifyAboutNewHistogramValue(self, hist): # type: (histogramContainer) -> N trend.extractTrendValue(hist) for alarm in trend.alarms: alarm.checkAlarm(trend) + From d1b88c62332c663e1847fa192bef29bf029873dc Mon Sep 17 00:00:00 2001 From: arturro96 Date: Thu, 29 Nov 2018 21:59:46 +0100 Subject: [PATCH 15/38] Added recipients in object.py --- overwatch/processing/alarms/collectors.py | 2 +- overwatch/processing/config.yaml | 20 ------------------- overwatch/processing/trending/info.py | 2 +- .../processing/trending/objects/object.py | 1 + 4 files changed, 3 insertions(+), 22 deletions(-) diff --git a/overwatch/processing/alarms/collectors.py b/overwatch/processing/alarms/collectors.py index f37e1144..1b393ab0 100644 --- a/overwatch/processing/alarms/collectors.py +++ b/overwatch/processing/alarms/collectors.py @@ -19,7 +19,7 @@ class Singleton(_Singleton('SingletonMeta', (object,), {})): class Mail(Singleton): def __init__(self, alarmsParameters=None): if alarmsParameters is not None: - smtpSettings = alarmsParameters["email_delivery"]["smtp_settings"] + smtpSettings = alarmsParameters["emailDelivery"]["smtp_settings"] host = smtpSettings["address"] port = smtpSettings["port"] password = smtpSettings["password"] diff --git a/overwatch/processing/config.yaml b/overwatch/processing/config.yaml index baf58ffb..b3013c8b 100644 --- a/overwatch/processing/config.yaml +++ b/overwatch/processing/config.yaml @@ -29,23 +29,3 @@ cumulativeMode: True # risk in changing it). dirPrefix: *dataFolder -# Email configuration -email_delivery: - delivery_method: :smtp - smtp_settings: - enable_starttls_auto: true - address: "smtp.gmail.com" - port: 587 - domain: "smtp.gmail.com" - authentication: :plain - user_name: "" - password: "" - -recipients: "" - -# Slack token -apiToken: '' - -# Slack channel -slackChannel: "" - diff --git a/overwatch/processing/trending/info.py b/overwatch/processing/trending/info.py index fd071f39..1c647f68 100644 --- a/overwatch/processing/trending/info.py +++ b/overwatch/processing/trending/info.py @@ -22,7 +22,7 @@ def __str__(self): class TrendingInfo: """ Container for data for TrendingObject - When TrendingInfo is initialized, data are validated. + When TrendingInfo is initialized, data is validated. """ __slots__ = ['name', 'desc', 'histogramNames', 'trendingClass', '_alarms'] diff --git a/overwatch/processing/trending/objects/object.py b/overwatch/processing/trending/objects/object.py index 67a19377..f4e6297c 100644 --- a/overwatch/processing/trending/objects/object.py +++ b/overwatch/processing/trending/objects/object.py @@ -30,6 +30,7 @@ def __init__(self, name, description, histogramNames, subsystemName, parameters) self.maxEntries = self.parameters.get(CON.ENTRIES, 100) self.trendedValues = self.initializeTrendingArray() self.alarms = [] + self.recipients = self.parameters["emailDelivery"]["recipients"][subsystemName] self.histogram = None # Ensure that the axis and points are drawn on the TGraph From 4ddabd7319a3abe0d0025952acc9d49691c61a31 Mon Sep 17 00:00:00 2001 From: ostro Date: Tue, 4 Dec 2018 22:46:05 +0100 Subject: [PATCH 16/38] possible: N trendingObject to N alarms, tests --- .../processing/alarms/aggregatingAlarm.py | 45 +++++++++ overwatch/processing/alarms/alarm.py | 21 +++- overwatch/processing/alarms/andAlarm.py | 19 ---- overwatch/processing/alarms/boarderAlarm.py | 17 ---- .../processing/alarms/changingValueAlarm.py | 26 ----- .../alarms/{test_alarms.py => example.py} | 13 +-- overwatch/processing/alarms/impl/__init__.py | 0 overwatch/processing/alarms/impl/andAlarm.py | 22 +++++ .../alarms/impl/betweenValuesAlarm.py | 21 ++++ .../alarms/impl/changingValueAlarm.py | 32 ++++++ .../alarms/{ => impl}/checkLastNAlarm.py | 50 +++++----- .../{ => impl}/checkLastNValuesAlarm.py | 18 ++-- overwatch/processing/alarms/impl/orAlarm.py | 23 +++++ overwatch/processing/alarms/orAlarm.py | 19 ---- overwatch/processing/alarms/registrator.py | 7 ++ tests/conftest.py | 1 + tests/processing/alarms/test_alarms.py | 33 +++++++ tests/processing/alarms/test_manyTrending.py | 99 +++++++++++++++++++ tests/processing/alarms/test_registrator.py | 17 ++++ tests/unit/fixtures/alarmFixtures.py | 57 +++++++++++ 20 files changed, 421 insertions(+), 119 deletions(-) create mode 100644 overwatch/processing/alarms/aggregatingAlarm.py delete mode 100644 overwatch/processing/alarms/andAlarm.py delete mode 100644 overwatch/processing/alarms/boarderAlarm.py delete mode 100644 overwatch/processing/alarms/changingValueAlarm.py rename overwatch/processing/alarms/{test_alarms.py => example.py} (68%) create mode 100644 overwatch/processing/alarms/impl/__init__.py create mode 100644 overwatch/processing/alarms/impl/andAlarm.py create mode 100644 overwatch/processing/alarms/impl/betweenValuesAlarm.py create mode 100644 overwatch/processing/alarms/impl/changingValueAlarm.py rename overwatch/processing/alarms/{ => impl}/checkLastNAlarm.py (58%) rename overwatch/processing/alarms/{ => impl}/checkLastNValuesAlarm.py (55%) create mode 100644 overwatch/processing/alarms/impl/orAlarm.py delete mode 100644 overwatch/processing/alarms/orAlarm.py create mode 100644 overwatch/processing/alarms/registrator.py create mode 100644 tests/processing/alarms/test_alarms.py create mode 100644 tests/processing/alarms/test_manyTrending.py create mode 100644 tests/processing/alarms/test_registrator.py create mode 100644 tests/unit/fixtures/alarmFixtures.py diff --git a/overwatch/processing/alarms/aggregatingAlarm.py b/overwatch/processing/alarms/aggregatingAlarm.py new file mode 100644 index 00000000..38be0c6f --- /dev/null +++ b/overwatch/processing/alarms/aggregatingAlarm.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +""" Base class for alarms which manage of aggregation alarms. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" + +from overwatch.processing.alarms.alarm import Alarm + +try: + from typing import * # noqa +except ImportError: + pass + + +class AggregatingAlarm(Alarm): + def __init__(self, children, alarmText=''): # type: (List[Alarm], str) -> None + super(AggregatingAlarm, self).__init__(alarmText=alarmText) + + # None - no value, True/False - last value returned from alarm + self.children = {c: None for c in children} # type: Dict[Alarm, Optional[bool]] + + for child in children: + child.parent = self + + def addChild(self, child): # type: (Alarm) -> None + assert child.parent is None + child.parent = self + self.children[child] = None + + def isAllAlarmsCompleted(self): + return all(c is not None for c in self.children.values()) + + def checkAlarm(self, *args, **kwargs): # type: () -> (bool, str) + """abstract method""" + raise NotImplementedError + + def childProcessed(self, child, result): + if self.children[child] is not None: + print("WARNING: last result ignored") + + self.children[child] = result + + if self.isAllAlarmsCompleted(): + self.processCheck() + self.children = {c: None for c in self.children} diff --git a/overwatch/processing/alarms/alarm.py b/overwatch/processing/alarms/alarm.py index a2bdc38e..1809473c 100644 --- a/overwatch/processing/alarms/alarm.py +++ b/overwatch/processing/alarms/alarm.py @@ -1,19 +1,36 @@ +#!/usr/bin/env python +""" Base class for alarms. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" + try: from typing import * # noqa except ImportError: - pass -else: + # Imports in this block below here are used solely for typing information from ..trending.objects.object import TrendingObject # noqa + from overwatch.processing.alarms.aggregatingAlarm import AggregatingAlarm # noqa class Alarm: def __init__(self, alarmText=''): self.alarmText = alarmText self.receivers = [] + self.parent = None # type: Optional[AggregatingAlarm] def addReceiver(self, receiver): # type: (callable) -> None self.receivers.append(receiver) + def processCheck(self, trend=None): # type: (Optional[TrendingObject]) -> None + args = (trend,) if trend else () + result = self.checkAlarm(*args) + isAlarm, msg = result + + if isAlarm: + self._announceAlarm(msg) + if self.parent: + self.parent.childProcessed(child=self, result=isAlarm) + def checkAlarm(self, trend): # type: (TrendingObject) -> bool """abstract method""" raise NotImplementedError diff --git a/overwatch/processing/alarms/andAlarm.py b/overwatch/processing/alarms/andAlarm.py deleted file mode 100644 index fda45664..00000000 --- a/overwatch/processing/alarms/andAlarm.py +++ /dev/null @@ -1,19 +0,0 @@ -from .alarm import Alarm - -try: - from typing import * # noqa -except ImportError: - pass - - -class AndAlarm(Alarm): - def __init__(self, alarmText='', *children): # type: (str, *Alarm) -> None - super(AndAlarm, self).__init__(alarmText=alarmText) - self.children = [] if not children else children - - def checkAlarm(self, trend): - for child in self.children: - if not child.checkAlarm(trend): - return False - - return True diff --git a/overwatch/processing/alarms/boarderAlarm.py b/overwatch/processing/alarms/boarderAlarm.py deleted file mode 100644 index e1ceb796..00000000 --- a/overwatch/processing/alarms/boarderAlarm.py +++ /dev/null @@ -1,17 +0,0 @@ -from .alarm import Alarm - - -class BorderAlarm(Alarm): - def __init__(self, minVal=0, maxVal=100, alarmText=''): - super(BorderAlarm, self).__init__(alarmText=alarmText) - self.minVal = minVal - self.maxVal = maxVal - - def checkAlarm(self, trend): - testedValue = trend.trendedValues[-1] - if self.minVal <= testedValue <= self.maxVal: - return False - - alarm = "value: {} not in [{}, {}]".format(testedValue, self.minVal, self.maxVal) - self._announceAlarm(alarm) - return True diff --git a/overwatch/processing/alarms/changingValueAlarm.py b/overwatch/processing/alarms/changingValueAlarm.py deleted file mode 100644 index 3caf7b73..00000000 --- a/overwatch/processing/alarms/changingValueAlarm.py +++ /dev/null @@ -1,26 +0,0 @@ -from .alarm import Alarm - - -class ChangingValueAlarm(Alarm): - def __init__(self, minVal=0, maxVal=100, ratio=2.0, * args, **kwargs): - super(ChangingValueAlarm, self).__init__(*args, **kwargs) - self.minVal = minVal - self.maxVal = maxVal - self.ratio = 2.0 - - def checkAlarm(self, trend): - if (len(trend.trendedValues < 2)): - return False - prevValue = trend.trendedValues[-2] - testedValue = trend.trendedValues[-1] - - if testedValue > 0 and prevValue > 0 and (testedValue <= self.ratio * prevValue - or prevValue * self.ratio <= testedValue): - return False - if testedValue < 0 and prevValue < 0 and (abs(prevValue) <= self.ratio * abs(testedValue) - or abs(testedValue) * self.ratio <= abs(prevValue)): - return False - - alarm = "value: {}, prev: {}, change more than: {}".format(testedValue, prevValue, self.ratio) - self._announceAlarm(alarm) - return True diff --git a/overwatch/processing/alarms/test_alarms.py b/overwatch/processing/alarms/example.py similarity index 68% rename from overwatch/processing/alarms/test_alarms.py rename to overwatch/processing/alarms/example.py index 13688ae5..9c08ffc7 100644 --- a/overwatch/processing/alarms/test_alarms.py +++ b/overwatch/processing/alarms/example.py @@ -1,6 +1,6 @@ from overwatch.processing.alarms.collectors import workerMail, printCollector, httpCollector, MailSender -from overwatch.processing.alarms.andAlarm import AndAlarm -from overwatch.processing.alarms.boarderAlarm import BorderAlarm +from overwatch.processing.alarms.impl.andAlarm import AndAlarm +from overwatch.processing.alarms.impl.betweenValuesAlarm import BetweenValuesAlarm class TrendingObjectMock: @@ -21,14 +21,15 @@ def __str__(self): def alarmConfig(): - boarderWarning = BorderAlarm(maxVal=50, alarmText="WARNING") + boarderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") boarderWarning.addReceiver(printCollector) - borderError = BorderAlarm(maxVal=70, alarmText="ERROR") + borderError = BetweenValuesAlarm(minVal=0, maxVal=70, alarmText="ERROR") borderError.receivers = [workerMail, httpCollector] - borderAlarm = BorderAlarm(maxVal=90) - seriousAlarm = AndAlarm("Serious Alarm", borderAlarm) + bva = BetweenValuesAlarm(minVal=0, maxVal=90, alarmText='BETWEEN') + # TODO add second alarm to andAlarm + seriousAlarm = AndAlarm([bva], "Serious Alarm") cernBoss = MailSender("boss@cern") seriousAlarm.addReceiver(cernBoss) diff --git a/overwatch/processing/alarms/impl/__init__.py b/overwatch/processing/alarms/impl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/overwatch/processing/alarms/impl/andAlarm.py b/overwatch/processing/alarms/impl/andAlarm.py new file mode 100644 index 00000000..826e616f --- /dev/null +++ b/overwatch/processing/alarms/impl/andAlarm.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +""" Alarms aggregation class - will notify if all alarms appeared. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" +from overwatch.processing.alarms.aggregatingAlarm import AggregatingAlarm +try: + from typing import * # noqa +except ImportError: + # Imports in this block below here are used solely for typing information + from overwatch.processing.alarms.alarm import Alarm # noqa + + +class AndAlarm(AggregatingAlarm): + def __init__(self, children, alarmText=''): # type: (list[Alarm], str) -> None + super(AndAlarm, self).__init__(children, alarmText=alarmText) + + def checkAlarm(self): + alarms = [alarm for alarm, val in self.children.items() if val] + result = len(alarms) == len(self.children) + msg = ", ".join(a.alarmText for a in alarms) + return result, msg diff --git a/overwatch/processing/alarms/impl/betweenValuesAlarm.py b/overwatch/processing/alarms/impl/betweenValuesAlarm.py new file mode 100644 index 00000000..1749fdde --- /dev/null +++ b/overwatch/processing/alarms/impl/betweenValuesAlarm.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +""" Check if trend is between minimal and maximal allowed values. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" +from overwatch.processing.alarms.alarm import Alarm + + +class BetweenValuesAlarm(Alarm): + def __init__(self, centerValue=50., maxDistance=50., minVal=None, maxVal=None, alarmText=''): + super(BetweenValuesAlarm, self).__init__(alarmText=alarmText) + self.minVal = minVal if minVal is not None else centerValue - maxDistance + self.maxVal = maxVal if maxVal is not None else centerValue + maxDistance + + def checkAlarm(self, trend): + testedValue = trend.trendedValues[-1] + if self.minVal <= testedValue <= self.maxVal: + return False, '' + + msg = "value: {} not in [{}, {}]".format(testedValue, self.minVal, self.maxVal) + return True, msg diff --git a/overwatch/processing/alarms/impl/changingValueAlarm.py b/overwatch/processing/alarms/impl/changingValueAlarm.py new file mode 100644 index 00000000..91e3bbff --- /dev/null +++ b/overwatch/processing/alarms/impl/changingValueAlarm.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +""" TODO add desc. + +.. code-author: Jacek Nabywaniec <>, AGH University of Science and Technology +""" +from overwatch.processing.alarms.alarm import Alarm + + +class ChangingValueAlarm(Alarm): + def __init__(self, minVal=0, maxVal=100, ratio=2.0, *args, **kwargs): + super(ChangingValueAlarm, self).__init__(*args, **kwargs) + self.minVal = minVal + self.maxVal = maxVal + self.ratio = ratio + + def checkAlarm(self, trend): + if len(trend.trendedValues < 2): + return False, '' + prevValue = trend.trendedValues[-2] + testedValue = trend.trendedValues[-1] + + if (testedValue > 0 and prevValue > 0 and + (testedValue <= self.ratio * prevValue or + prevValue * self.ratio <= testedValue)): + return False, '' + if (testedValue < 0 and prevValue < 0 and + (abs(prevValue) <= self.ratio * abs(testedValue) or + abs(testedValue) * self.ratio <= abs(prevValue))): + return False, '' + + msg = "value: {}, prev: {}, change more than: {}".format(testedValue, prevValue, self.ratio) + return True, msg diff --git a/overwatch/processing/alarms/checkLastNAlarm.py b/overwatch/processing/alarms/impl/checkLastNAlarm.py similarity index 58% rename from overwatch/processing/alarms/checkLastNAlarm.py rename to overwatch/processing/alarms/impl/checkLastNAlarm.py index 773704a2..401c4479 100644 --- a/overwatch/processing/alarms/checkLastNAlarm.py +++ b/overwatch/processing/alarms/impl/checkLastNAlarm.py @@ -1,23 +1,27 @@ -from .alarm import Alarm - - -class checkLastNAlarm(Alarm): - def __init__(self, minVal=0, maxVal=100, ratio=0.6, N=5, *args, **kwargs): - super(checkLastNAlarm, self).__init__(*args, **kwargs) - self.minVal = minVal - self.maxVal = maxVal - self.ratio = ratio - self.N = N - - def checkAlarm(self, trend): - if len(trend.trendedValues) < self.N: - return False - trendedValues = trend[-self.N:] - inBorderValues = [trendedValue for trendedValue in trendedValues if self.maxVal > trendedValue > self.minVal] - if len(inBorderValues) >= self.ratio * self.N: - return False - - alarm = "less than {} % values of last: {} values not in {} {}".format(self.ratio * 10, self.N, self.minVal, - self.maxVal) - self._announceAlarm(alarm) - return True +#!/usr/bin/env python +""" TODO add desc. + +.. code-author: Jacek Nabywaniec <>, AGH University of Science and Technology +""" +from overwatch.processing.alarms.alarm import Alarm + + +class checkLastNAlarm(Alarm): + def __init__(self, minVal=0, maxVal=100, ratio=0.6, N=5, *args, **kwargs): + super(checkLastNAlarm, self).__init__(*args, **kwargs) + self.minVal = minVal + self.maxVal = maxVal + self.ratio = ratio + self.N = N + + def checkAlarm(self, trend): + if len(trend.trendedValues) < self.N: + return False, '' + trendedValues = trend[-self.N:] + inBorderValues = [trendedValue for trendedValue in trendedValues if self.maxVal > trendedValue > self.minVal] + if len(inBorderValues) >= self.ratio * self.N: + return False, '' + + msg = "less than {} % values of last: {} values not in {} {}".format( + self.ratio * 10, self.N, self.minVal, self.maxVal) + return True, msg diff --git a/overwatch/processing/alarms/checkLastNValuesAlarm.py b/overwatch/processing/alarms/impl/checkLastNValuesAlarm.py similarity index 55% rename from overwatch/processing/alarms/checkLastNValuesAlarm.py rename to overwatch/processing/alarms/impl/checkLastNValuesAlarm.py index a2f55f66..3a094c2a 100644 --- a/overwatch/processing/alarms/checkLastNValuesAlarm.py +++ b/overwatch/processing/alarms/impl/checkLastNValuesAlarm.py @@ -1,4 +1,9 @@ -from .alarm import Alarm +#!/usr/bin/env python +""" TODO add desc. + +.. code-author: Jacek Nabywaniec <>, AGH University of Science and Technology +""" +from overwatch.processing.alarms.alarm import Alarm import numpy as np @@ -7,16 +12,15 @@ def __init__(self, minVal=0, maxVal=100, N=5, *args, **kwargs): super(checkLastNValuesAlarm, self).__init__(*args, **kwargs) self.minVal = minVal self.maxVal = maxVal - self.N = 5 + self.N = N def checkAlarm(self, trend): if len(trend.trendedValues) < self.N: - return False + return False, '' trendedValues = np.array(trend.trendedValues) mean = np.mean(trendedValues) if self.minVal < np.mean(mean) < self.maxVal: - return False + return False, '' - alarm = "mean value of last: {} values not in {} {}".format(self.N, self.minVal, self.maxVal) - self._announceAlarm(alarm) - return True + msg = "mean value of last: {} values not in {} {}".format(self.N, self.minVal, self.maxVal) + return True, msg diff --git a/overwatch/processing/alarms/impl/orAlarm.py b/overwatch/processing/alarms/impl/orAlarm.py new file mode 100644 index 00000000..5d40f5fc --- /dev/null +++ b/overwatch/processing/alarms/impl/orAlarm.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +""" Alarms aggregation class - will notify if any alarm appeared. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" +from overwatch.processing.alarms.aggregatingAlarm import AggregatingAlarm + +try: + from typing import * # noqa +except ImportError: + # Imports in this block below here are used solely for typing information + from overwatch.processing.alarms.alarm import Alarm # noqa + + +class OrAlarm(AggregatingAlarm): + def __init__(self, children, alarmText=''): # type: (List[Alarm], str) -> None + super(OrAlarm, self).__init__(children, alarmText=alarmText) + + def checkAlarm(self): + alarms = [alarm for alarm, val in self.children.items() if val] + result = len(alarms) > 0 + msg = ", ".join(a.alarmText for a in alarms) + return result, msg diff --git a/overwatch/processing/alarms/orAlarm.py b/overwatch/processing/alarms/orAlarm.py deleted file mode 100644 index a689ebad..00000000 --- a/overwatch/processing/alarms/orAlarm.py +++ /dev/null @@ -1,19 +0,0 @@ -from .alarm import Alarm - -try: - from typing import * # noqa -except ImportError: - pass - - -class OrAlarm(Alarm): - def __init__(self, alarmText='', *children): # type: (str, *Alarm) -> None - super(OrAlarm, self).__init__(alarmText=alarmText) - self.children = [] if not children else children - - def checkAlarm(self, trend): - for child in self.children: - if child.checkAlarm(trend): - return True - - return False diff --git a/overwatch/processing/alarms/registrator.py b/overwatch/processing/alarms/registrator.py new file mode 100644 index 00000000..c653e58e --- /dev/null +++ b/overwatch/processing/alarms/registrator.py @@ -0,0 +1,7 @@ + +registeredAlarms = dict() + + +def getOrRegisterAlarm(alarmName, alarm): # type: (str, 'Alarm') -> 'Alarm' + """Register global alarm or return existing""" + return registeredAlarms.setdefault(alarmName, alarm) diff --git a/tests/conftest.py b/tests/conftest.py index fcb19941..4420716d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,4 +4,5 @@ pytest_plugins = [ "tests.unit.fixtures.loggingPlugin", "tests.unit.fixtures.trendingFixtures", + "tests.unit.fixtures.alarmFixtures", ] diff --git a/tests/processing/alarms/test_alarms.py b/tests/processing/alarms/test_alarms.py new file mode 100644 index 00000000..fd2426a6 --- /dev/null +++ b/tests/processing/alarms/test_alarms.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +""" Tests for alarm implementations. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" +import pytest + +from overwatch.processing.alarms.impl.betweenValuesAlarm import BetweenValuesAlarm + + +@pytest.mark.parametrize('alarm', [ + BetweenValuesAlarm(centerValue=50., maxDistance=50.), + BetweenValuesAlarm(minVal=0., maxVal=100.), +]) +def testBetweenValuesAlarm(af_alarmChecker, af_trendingObjectClass, alarm): + alarm.addReceiver(af_alarmChecker.receiver) + to = af_trendingObjectClass() + to.alarms = [alarm] + + def test(val, isAlarm=False): + af_alarmChecker.addValueAndCheck(to, val, isAlarm) + + test(3) + test(-10, True) + test(17) + test(0) + test(105, True) + test(100) + test(44) + test(-1, True) + test(101, True) + +# TODO test more class diff --git a/tests/processing/alarms/test_manyTrending.py b/tests/processing/alarms/test_manyTrending.py new file mode 100644 index 00000000..6e4395e5 --- /dev/null +++ b/tests/processing/alarms/test_manyTrending.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +""" Tests for N trending object to N alarms. + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" + +from overwatch.processing.alarms.impl.andAlarm import AndAlarm +from overwatch.processing.alarms.impl.betweenValuesAlarm import BetweenValuesAlarm +from overwatch.processing.alarms.impl.orAlarm import OrAlarm +try: + from typing import * # noqa +except ImportError: + # Imports in this block below here are used solely for typing information + from tests.unit.fixtures.alarmFixtures import TrendingObjectMock # noqa + + +def testManyTrendingObjectToOneAlarm(af_alarmChecker, af_trendingObjectClass): + tc1 = af_trendingObjectClass('tc1') # type: TrendingObjectMock + tc2 = af_trendingObjectClass('tc2') # type: TrendingObjectMock + + ba1 = BetweenValuesAlarm(minVal=10, maxVal=30, alarmText='ba1') + ba2 = BetweenValuesAlarm(minVal=40, maxVal=50, alarmText='ba2') + andAlarm = AndAlarm([ba1, ba2], 'andAlarm') + andAlarm.addReceiver(af_alarmChecker.receiver) + + tc1.alarms = [ba1] + tc2.alarms = [ba2] + + af_alarmChecker.addValueAndCheck(tc1, 1, False) + af_alarmChecker.addValueAndCheck(tc2, 2, True) + + af_alarmChecker.addValueAndCheck(tc1, 3) + af_alarmChecker.addValueAndCheck(tc2, 45) + + af_alarmChecker.addValueAndCheck(tc1, 25) + af_alarmChecker.addValueAndCheck(tc2, 4) + + af_alarmChecker.addValueAndCheck(tc1, 26) + af_alarmChecker.addValueAndCheck(tc2, 46) + + af_alarmChecker.addValueAndCheck(tc1, 100, False) + af_alarmChecker.addValueAndCheck(tc2, 200, True) + + af_alarmChecker.addValueAndCheck(tc2, 300, False) + af_alarmChecker.addValueAndCheck(tc1, 400, True) + + +def testOneTrendingObjectToManyAlarm(af_alarmChecker, af_trendingObjectClass): + warning = BetweenValuesAlarm(minVal=30, maxVal=40, alarmText='error') + warning.addReceiver(af_alarmChecker.receiver) + error = BetweenValuesAlarm(minVal=20, maxVal=50, alarmText='warning') + error.addReceiver(af_alarmChecker.receiver) + + to = af_trendingObjectClass('to') # type: TrendingObjectMock + to.alarms = [error, warning] + + af_alarmChecker.addValueAndCheck(to, 35) + af_alarmChecker.addValueAndCheck(to, 30) + af_alarmChecker.addValueAndCheck(to, 40) + + af_alarmChecker.addValueAndCheck(to, 25, 1) + af_alarmChecker.addValueAndCheck(to, 45, 1) + + af_alarmChecker.addValueAndCheck(to, 15, 2) + af_alarmChecker.addValueAndCheck(to, 55, 2) + + +def testManyTrendingObjectToManyAlarm(af_alarmChecker, af_trendingObjectClass): + error1 = BetweenValuesAlarm(minVal=20, maxVal=50, alarmText='errorTO1') + error2 = BetweenValuesAlarm(minVal=20, maxVal=50, alarmText='errorTO2') + + warning1 = BetweenValuesAlarm(minVal=30, maxVal=40, alarmText='warningTO1') + warning2 = BetweenValuesAlarm(minVal=30, maxVal=40, alarmText='warningTO1') + + tc1 = af_trendingObjectClass('tc1') # type: TrendingObjectMock + tc1.alarms = [error1, warning1] + tc2 = af_trendingObjectClass('tc2') # type: TrendingObjectMock + tc2.alarms = [error2, warning2] + + # alarm will be if any of errors appear or all warnings + andAlarm = AndAlarm([warning1, warning2], alarmText='AndWarning') + orAlarm = OrAlarm([error1, error2], alarmText='OrError') + rootAlarm = OrAlarm([andAlarm, orAlarm], alarmText='RootAlarm') + rootAlarm.addReceiver(af_alarmChecker.receiver) + + def check(val1, val2, isAlarm=False): + af_alarmChecker.addValueAndCheck(tc1, val1) + af_alarmChecker.addValueAndCheck(tc2, val2, isAlarm) + + # it doesn't matter which trending object will be first + af_alarmChecker.addValueAndCheck(tc2, val2) + af_alarmChecker.addValueAndCheck(tc1, val1, isAlarm) + + check(35, 36, False) # all in range + check(41, 30, False) # 1 warning + check(42, 29, True) # 2 warnings + check(53, 35, True) # 1 error (1 warning) + check(17, 45, True) # 1 error (2 warning) + check(54, 18, True) # 2 errors (2 warnings) diff --git a/tests/processing/alarms/test_registrator.py b/tests/processing/alarms/test_registrator.py new file mode 100644 index 00000000..61d17d1b --- /dev/null +++ b/tests/processing/alarms/test_registrator.py @@ -0,0 +1,17 @@ +from overwatch.processing.alarms.registrator import registeredAlarms, getOrRegisterAlarm +from overwatch.processing.alarms.alarm import Alarm + + +def test_register(): + assert len(registeredAlarms) == 0 + alarmName = 'register test' + + alarm1 = getOrRegisterAlarm(alarmName, Alarm(alarmText=alarmName)) + assert len(registeredAlarms) == 1 + + getOrRegisterAlarm('other alarm', Alarm(alarmText=alarmName)) + assert len(registeredAlarms) == 2 + + alarm2 = getOrRegisterAlarm(alarmName, Alarm(alarmText=alarmName)) + assert len(registeredAlarms) == 2 + assert alarm1 is alarm2 diff --git a/tests/unit/fixtures/alarmFixtures.py b/tests/unit/fixtures/alarmFixtures.py new file mode 100644 index 00000000..eaf61863 --- /dev/null +++ b/tests/unit/fixtures/alarmFixtures.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +""" Fixtures for alarms. + +All fixtures for alarms have 'af_' prefix + +.. code-author: Pawel Ostrowski , AGH University of Science and Technology +""" + +try: + from typing import * # noqa +except ImportError: + pass + +import pytest + + +class TrendingObjectMock: + def __init__(self, name=''): + self.name = name + self.alarms = [] + self.trendedValues = [] + + def addNewValue(self, val): + self.trendedValues.append(val) + self.checkAlarms() + + def checkAlarms(self): + for alarm in self.alarms: + alarm.processCheck(self) + + +@pytest.fixture +def af_trendingObjectClass(): + yield TrendingObjectMock + + +class AlarmChecker(object): + def __init__(self): + self.receivedAlarms = [] + self.counter = 0 + + def receiver(self, msg): # type: (str) -> None + self.receivedAlarms.append(msg) + + def addValueAndCheck(self, trendingObject, value, isAlarm=False): + # type: ('to', float, Union[bool, int])->None + trendingObject.addNewValue(value) + if isinstance(isAlarm, bool): + self.counter += 1 if isAlarm else 0 + else: + self.counter += isAlarm + assert len(self.receivedAlarms) == self.counter + + +@pytest.fixture +def af_alarmChecker(): + yield AlarmChecker() From 581b42624c72116f389cc94f80cc5c4e5c364231 Mon Sep 17 00:00:00 2001 From: ostro Date: Wed, 5 Dec 2018 19:31:56 +0100 Subject: [PATCH 17/38] setAlarms fix --- overwatch/processing/trending/info.py | 16 ++++++++++------ overwatch/processing/trending/manager.py | 1 + overwatch/processing/trending/objects/object.py | 6 +++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/overwatch/processing/trending/info.py b/overwatch/processing/trending/info.py index cc479dc9..e2e20229 100644 --- a/overwatch/processing/trending/info.py +++ b/overwatch/processing/trending/info.py @@ -3,15 +3,16 @@ .. code-author: Pawel Ostrowski , AGH University of Science and Technology """ +import past.builtins + +from overwatch.processing.alarms.alarm import Alarm +from overwatch.processing.trending.objects.object import TrendingObject + try: from typing import * # noqa - from overwatch.processing.alarms.alarm import Alarm # noqa except ImportError: pass -from overwatch.processing.trending.objects.object import TrendingObject -import past.builtins - basestring = past.builtins.basestring @@ -40,7 +41,7 @@ def __init__(self, name, desc, histogramNames, trendingClass): trendingClass: concrete class of abstract class TrendingObject """ # type: (str, str, List[str], Type[TrendingObject]) -> None - # trending objects within subsystem must have different names - TODO add validation? + # trending objects within subsystem must have different names self.name = self._validate(name) self.desc = self._validate(desc) self.histogramNames = self._validateHist(histogramNames) @@ -49,7 +50,10 @@ def __init__(self, name, desc, histogramNames, trendingClass): self._alarms = [] def addAlarm(self, alarm): # type: (Alarm) -> None - self._alarms.append(alarm) + if isinstance(alarm, Alarm): + self._alarms.append(alarm) + else: + raise TrendingInfoException(msg='WrongAlarmType') def createTrendingClass(self, subsystemName, parameters): # type: (str, dict) -> TrendingObject """Create instance of TrendingObject from previously set parameters diff --git a/overwatch/processing/trending/manager.py b/overwatch/processing/trending/manager.py index 755136e2..b35033f2 100644 --- a/overwatch/processing/trending/manager.py +++ b/overwatch/processing/trending/manager.py @@ -125,6 +125,7 @@ def _subscribe(self, trendingObject, histogramNames): # type: (TrendingObject, def resetDB(self): # TODO not used - is it needed? self.trendingDB.clear() + self._prepareDirStructure() def processTrending(self): """ Process the trending objects. diff --git a/overwatch/processing/trending/objects/object.py b/overwatch/processing/trending/objects/object.py index 4f55bae2..0cb973b5 100644 --- a/overwatch/processing/trending/objects/object.py +++ b/overwatch/processing/trending/objects/object.py @@ -111,6 +111,6 @@ def resetCanvas(canvas): canvas.SetLogy(False) canvas.SetLogz(False) - def addAlarms(self, alarms): # type: (List['Alarm']) -> None - """ ADD DOC""" - self.alarms.extend(alarms) + def setAlarms(self, alarms): # type: (List['Alarm']) -> None + """ Invoked by trendingInfo after trendingObject is created""" + self.alarms = alarms From 2ae77bcc9e63789fbebe06c1f313eaecb0333d7e Mon Sep 17 00:00:00 2001 From: ostro Date: Wed, 5 Dec 2018 21:25:03 +0100 Subject: [PATCH 18/38] change alarm names, convert python list to numpy --- overwatch/processing/alarms/alarm.py | 14 ++++++-- .../alarms/impl/betweenValuesAlarm.py | 2 +- .../alarms/impl/changingValueAlarm.py | 32 ------------------- .../processing/alarms/impl/checkLastNAlarm.py | 11 ++++--- ...astNValuesAlarm.py => meanInRangeAlarm.py} | 14 ++++---- .../alarms/impl/previousValueAlarm.py | 32 +++++++++++++++++++ overwatch/processing/trending/manager.py | 3 +- 7 files changed, 61 insertions(+), 47 deletions(-) delete mode 100644 overwatch/processing/alarms/impl/changingValueAlarm.py rename overwatch/processing/alarms/impl/{checkLastNValuesAlarm.py => meanInRangeAlarm.py} (59%) create mode 100644 overwatch/processing/alarms/impl/previousValueAlarm.py diff --git a/overwatch/processing/alarms/alarm.py b/overwatch/processing/alarms/alarm.py index 1809473c..be5a6cff 100644 --- a/overwatch/processing/alarms/alarm.py +++ b/overwatch/processing/alarms/alarm.py @@ -3,6 +3,7 @@ .. code-author: Pawel Ostrowski , AGH University of Science and Technology """ +import numpy as np try: from typing import * # noqa @@ -22,7 +23,7 @@ def addReceiver(self, receiver): # type: (callable) -> None self.receivers.append(receiver) def processCheck(self, trend=None): # type: (Optional[TrendingObject]) -> None - args = (trend,) if trend else () + args = (self.prepareTrendValues(trend),) if trend else () result = self.checkAlarm(*args) isAlarm, msg = result @@ -31,7 +32,16 @@ def processCheck(self, trend=None): # type: (Optional[TrendingObject]) -> None if self.parent: self.parent.childProcessed(child=self, result=isAlarm) - def checkAlarm(self, trend): # type: (TrendingObject) -> bool + @staticmethod + def prepareTrendValues(trend): # type: (TrendingObject) -> np.ndarray + trendingValues = np.array(trend.trendedValues) + if len(trendingValues.shape) == 2: + trendingValues = trendingValues[:, 0] + if len(trendingValues.shape) > 2: + raise TypeError + return trendingValues + + def checkAlarm(self, trend): # type: (np.ndarray) -> (bool, str) """abstract method""" raise NotImplementedError diff --git a/overwatch/processing/alarms/impl/betweenValuesAlarm.py b/overwatch/processing/alarms/impl/betweenValuesAlarm.py index 1749fdde..8e6ce486 100644 --- a/overwatch/processing/alarms/impl/betweenValuesAlarm.py +++ b/overwatch/processing/alarms/impl/betweenValuesAlarm.py @@ -13,7 +13,7 @@ def __init__(self, centerValue=50., maxDistance=50., minVal=None, maxVal=None, a self.maxVal = maxVal if maxVal is not None else centerValue + maxDistance def checkAlarm(self, trend): - testedValue = trend.trendedValues[-1] + testedValue = trend[-1] if self.minVal <= testedValue <= self.maxVal: return False, '' diff --git a/overwatch/processing/alarms/impl/changingValueAlarm.py b/overwatch/processing/alarms/impl/changingValueAlarm.py deleted file mode 100644 index 91e3bbff..00000000 --- a/overwatch/processing/alarms/impl/changingValueAlarm.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python -""" TODO add desc. - -.. code-author: Jacek Nabywaniec <>, AGH University of Science and Technology -""" -from overwatch.processing.alarms.alarm import Alarm - - -class ChangingValueAlarm(Alarm): - def __init__(self, minVal=0, maxVal=100, ratio=2.0, *args, **kwargs): - super(ChangingValueAlarm, self).__init__(*args, **kwargs) - self.minVal = minVal - self.maxVal = maxVal - self.ratio = ratio - - def checkAlarm(self, trend): - if len(trend.trendedValues < 2): - return False, '' - prevValue = trend.trendedValues[-2] - testedValue = trend.trendedValues[-1] - - if (testedValue > 0 and prevValue > 0 and - (testedValue <= self.ratio * prevValue or - prevValue * self.ratio <= testedValue)): - return False, '' - if (testedValue < 0 and prevValue < 0 and - (abs(prevValue) <= self.ratio * abs(testedValue) or - abs(testedValue) * self.ratio <= abs(prevValue))): - return False, '' - - msg = "value: {}, prev: {}, change more than: {}".format(testedValue, prevValue, self.ratio) - return True, msg diff --git a/overwatch/processing/alarms/impl/checkLastNAlarm.py b/overwatch/processing/alarms/impl/checkLastNAlarm.py index 401c4479..05f06a80 100644 --- a/overwatch/processing/alarms/impl/checkLastNAlarm.py +++ b/overwatch/processing/alarms/impl/checkLastNAlarm.py @@ -1,24 +1,25 @@ #!/usr/bin/env python -""" TODO add desc. +""" Check if minimum ratio*N last N alarms are in range. .. code-author: Jacek Nabywaniec <>, AGH University of Science and Technology """ from overwatch.processing.alarms.alarm import Alarm -class checkLastNAlarm(Alarm): +class CheckLastNAlarm(Alarm): def __init__(self, minVal=0, maxVal=100, ratio=0.6, N=5, *args, **kwargs): - super(checkLastNAlarm, self).__init__(*args, **kwargs) + super(CheckLastNAlarm, self).__init__(*args, **kwargs) self.minVal = minVal self.maxVal = maxVal self.ratio = ratio self.N = N def checkAlarm(self, trend): - if len(trend.trendedValues) < self.N: + if len(trend) < self.N: return False, '' + trendedValues = trend[-self.N:] - inBorderValues = [trendedValue for trendedValue in trendedValues if self.maxVal > trendedValue > self.minVal] + inBorderValues = [tv for tv in trendedValues if self.maxVal > tv > self.minVal] if len(inBorderValues) >= self.ratio * self.N: return False, '' diff --git a/overwatch/processing/alarms/impl/checkLastNValuesAlarm.py b/overwatch/processing/alarms/impl/meanInRangeAlarm.py similarity index 59% rename from overwatch/processing/alarms/impl/checkLastNValuesAlarm.py rename to overwatch/processing/alarms/impl/meanInRangeAlarm.py index 3a094c2a..f3efb5d3 100644 --- a/overwatch/processing/alarms/impl/checkLastNValuesAlarm.py +++ b/overwatch/processing/alarms/impl/meanInRangeAlarm.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -""" TODO add desc. +""" Check if mean from N last measurements is in the range. .. code-author: Jacek Nabywaniec <>, AGH University of Science and Technology """ @@ -7,20 +7,22 @@ import numpy as np -class checkLastNValuesAlarm(Alarm): +class MeanInRangeAlarm(Alarm): def __init__(self, minVal=0, maxVal=100, N=5, *args, **kwargs): - super(checkLastNValuesAlarm, self).__init__(*args, **kwargs) + super(MeanInRangeAlarm, self).__init__(*args, **kwargs) self.minVal = minVal self.maxVal = maxVal self.N = N def checkAlarm(self, trend): - if len(trend.trendedValues) < self.N: + if len(trend) < self.N: return False, '' - trendedValues = np.array(trend.trendedValues) + + trendedValues = trend[-self.N:] mean = np.mean(trendedValues) if self.minVal < np.mean(mean) < self.maxVal: return False, '' - msg = "mean value of last: {} values not in {} {}".format(self.N, self.minVal, self.maxVal) + msg = "mean value of last: {n} values not in {min} {max}".format( + n=self.N, min=self.minVal, max=self.maxVal) return True, msg diff --git a/overwatch/processing/alarms/impl/previousValueAlarm.py b/overwatch/processing/alarms/impl/previousValueAlarm.py new file mode 100644 index 00000000..6ee7b6c6 --- /dev/null +++ b/overwatch/processing/alarms/impl/previousValueAlarm.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +""" Check if last value is different more than ratio*previous value. + +.. code-author: Jacek Nabywaniec <>, AGH University of Science and Technology +""" +from overwatch.processing.alarms.alarm import Alarm + + +class PreviousValueAlarm(Alarm): + def __init__(self, minVal=0, maxVal=100, ratio=2.0, *args, **kwargs): + super(PreviousValueAlarm, self).__init__(*args, **kwargs) + self.minVal = minVal + self.maxVal = maxVal + self.ratio = ratio + + def checkAlarm(self, trend): + if len(trend) < 2: + return False, '' + prevValue = trend[-2] + curValue = trend[-1] + + if (curValue > 0 and prevValue > 0 and + (curValue <= self.ratio * prevValue or + prevValue * self.ratio <= curValue)): + return False, '' + if (curValue < 0 and prevValue < 0 and + (abs(prevValue) <= self.ratio * abs(curValue) or + abs(curValue) * self.ratio <= abs(prevValue))): + return False, '' + + msg = "value: {}, prev: {}, change more than: {}".format(curValue, prevValue, self.ratio) + return True, msg diff --git a/overwatch/processing/trending/manager.py b/overwatch/processing/trending/manager.py index b35033f2..9d8166fb 100644 --- a/overwatch/processing/trending/manager.py +++ b/overwatch/processing/trending/manager.py @@ -152,6 +152,7 @@ def notifyAboutNewHistogramValue(self, hist): # type: (histogramContainer) -> N It loops over trending objects to which histogram is subscribed to and calls function that extracts trended value from histogram e.g. mean, standard deviation (depending on trending object). + Then check alarms. Args: hist (histogramContainer): Histogram which is processed. @@ -161,4 +162,4 @@ def notifyAboutNewHistogramValue(self, hist): # type: (histogramContainer) -> N for trend in self.histToTrending.get(hist.histName, []): trend.extractTrendValue(hist) for alarm in trend.alarms: - alarm.checkAlarm(trend) + alarm.processCheck(trend) From 634cb6d260169938c9f21621f7331ff69a3f74d1 Mon Sep 17 00:00:00 2001 From: ostro Date: Thu, 6 Dec 2018 23:19:02 +0100 Subject: [PATCH 19/38] split previousValue into absolute and relative --- .../alarms/impl/absolutePreviousValueAlarm.py | 26 ++++++++ .../alarms/impl/previousValueAlarm.py | 32 ---------- .../alarms/impl/relativePreviousValueAlarm.py | 28 +++++++++ tests/processing/alarms/test_alarms.py | 59 ++++++++++++++++++- 4 files changed, 112 insertions(+), 33 deletions(-) create mode 100644 overwatch/processing/alarms/impl/absolutePreviousValueAlarm.py delete mode 100644 overwatch/processing/alarms/impl/previousValueAlarm.py create mode 100644 overwatch/processing/alarms/impl/relativePreviousValueAlarm.py diff --git a/overwatch/processing/alarms/impl/absolutePreviousValueAlarm.py b/overwatch/processing/alarms/impl/absolutePreviousValueAlarm.py new file mode 100644 index 00000000..38dd8570 --- /dev/null +++ b/overwatch/processing/alarms/impl/absolutePreviousValueAlarm.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +""" Check if (new value - old value) is different more than delta. + +.. code-author: Jacek Nabywaniec <>, AGH University of Science and Technology +""" +from overwatch.processing.alarms.alarm import Alarm + + +class AbsolutePreviousValueAlarm(Alarm): + def __init__(self, maxDelta=1, *args, **kwargs): + super(AbsolutePreviousValueAlarm, self).__init__(*args, **kwargs) + self.maxDelta = maxDelta + + def checkAlarm(self, trend): + if len(trend) < 2: + return False, '' + prevValue = trend[-2] + curValue = trend[-1] + + delta = abs(prevValue - curValue) + if delta <= self.maxDelta: + return False, '' + + msg = "curValue: {curValue}, prevValue: {prevValue}, change more than: {maxDelta}".format( + curValue=curValue, prevValue=prevValue, maxDelta=self.maxDelta) + return True, msg diff --git a/overwatch/processing/alarms/impl/previousValueAlarm.py b/overwatch/processing/alarms/impl/previousValueAlarm.py deleted file mode 100644 index 6ee7b6c6..00000000 --- a/overwatch/processing/alarms/impl/previousValueAlarm.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python -""" Check if last value is different more than ratio*previous value. - -.. code-author: Jacek Nabywaniec <>, AGH University of Science and Technology -""" -from overwatch.processing.alarms.alarm import Alarm - - -class PreviousValueAlarm(Alarm): - def __init__(self, minVal=0, maxVal=100, ratio=2.0, *args, **kwargs): - super(PreviousValueAlarm, self).__init__(*args, **kwargs) - self.minVal = minVal - self.maxVal = maxVal - self.ratio = ratio - - def checkAlarm(self, trend): - if len(trend) < 2: - return False, '' - prevValue = trend[-2] - curValue = trend[-1] - - if (curValue > 0 and prevValue > 0 and - (curValue <= self.ratio * prevValue or - prevValue * self.ratio <= curValue)): - return False, '' - if (curValue < 0 and prevValue < 0 and - (abs(prevValue) <= self.ratio * abs(curValue) or - abs(curValue) * self.ratio <= abs(prevValue))): - return False, '' - - msg = "value: {}, prev: {}, change more than: {}".format(curValue, prevValue, self.ratio) - return True, msg diff --git a/overwatch/processing/alarms/impl/relativePreviousValueAlarm.py b/overwatch/processing/alarms/impl/relativePreviousValueAlarm.py new file mode 100644 index 00000000..6d5073a7 --- /dev/null +++ b/overwatch/processing/alarms/impl/relativePreviousValueAlarm.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +""" Check if new value is between (previous value)/ratio and (previous value)*ratio. + +.. code-author: Jacek Nabywaniec <>, AGH University of Science and Technology +""" +from overwatch.processing.alarms.alarm import Alarm + + +class RelativePreviousValueAlarm(Alarm): + def __init__(self, ratio=2.0, *args, **kwargs): + super(RelativePreviousValueAlarm, self).__init__(*args, **kwargs) + assert ratio > 1 + self.ratio = ratio + + def checkAlarm(self, trend): + if len(trend) < 2: + return False, '' + prevValue = trend[-2] + curValue = trend[-1] + + if prevValue < 0: + curValue = -curValue + if abs(prevValue / self.ratio) <= curValue <= abs(prevValue * self.ratio): + return False, '' + + msg = "curValue: {curValue}, prevValue: {prevValue}, change more than: {ratio}".format( + curValue=curValue, prevValue=prevValue, ratio=self.ratio) + return True, msg diff --git a/tests/processing/alarms/test_alarms.py b/tests/processing/alarms/test_alarms.py index fd2426a6..3371588c 100644 --- a/tests/processing/alarms/test_alarms.py +++ b/tests/processing/alarms/test_alarms.py @@ -5,7 +5,9 @@ """ import pytest +from overwatch.processing.alarms.impl.absolutePreviousValueAlarm import AbsolutePreviousValueAlarm from overwatch.processing.alarms.impl.betweenValuesAlarm import BetweenValuesAlarm +from overwatch.processing.alarms.impl.relativePreviousValueAlarm import RelativePreviousValueAlarm @pytest.mark.parametrize('alarm', [ @@ -30,4 +32,59 @@ def test(val, isAlarm=False): test(-1, True) test(101, True) -# TODO test more class + +def testRelativePreviousValueAlarm(af_alarmChecker, af_trendingObjectClass): + alarm = RelativePreviousValueAlarm(ratio=2.) + alarm.addReceiver(af_alarmChecker.receiver) + to = af_trendingObjectClass() + to.alarms = [alarm] + + def test(val, isAlarm=False): + af_alarmChecker.addValueAndCheck(to, val, isAlarm) + + test(6) + test(7) + test(14) + test(7) + test(15, True) + test(16) + test(7, True) + test(0.1, True) + test(0, True) + test(-0.1, True) + test(2, True) + test(-2, True) + test(-1) + test(-2) + test(-4) + test(-1, True) + test(-3, True) + test(-6) + + +def testAbsolutePreviousValueAlarm(af_alarmChecker, af_trendingObjectClass): + alarm = AbsolutePreviousValueAlarm(maxDelta=3.) + alarm.addReceiver(af_alarmChecker.receiver) + to = af_trendingObjectClass() + to.alarms = [alarm] + + def test(val, isAlarm=False): + af_alarmChecker.addValueAndCheck(to, val, isAlarm) + + test(34) + test(33) + test(35) + test(32) + test(36, True) + test(30, True) + test(29) + test(10, True) + test(9) + test(15, True) + test(0, True) + test(-1) + test(1) + test(0) + test(-17, True) + test(-15) + test(-9, True) From 3238ee6303b2f57cf2d6acb8898f75662fb75bdd Mon Sep 17 00:00:00 2001 From: arturro96 Date: Sat, 8 Dec 2018 14:16:20 +0100 Subject: [PATCH 20/38] Added alarm collector and docstrings --- overwatch/processing/alarms/alarm.py | 4 +- overwatch/processing/alarms/collectors.py | 109 +++++++++++++++++++--- 2 files changed, 98 insertions(+), 15 deletions(-) diff --git a/overwatch/processing/alarms/alarm.py b/overwatch/processing/alarms/alarm.py index 6cc3f4ed..25f3bd5c 100644 --- a/overwatch/processing/alarms/alarm.py +++ b/overwatch/processing/alarms/alarm.py @@ -4,6 +4,7 @@ .. code-author: Pawel Ostrowski , AGH University of Science and Technology """ import numpy as np +from overwatch.processing.alarms.collectors import alarmCollector try: from typing import * # noqa @@ -26,9 +27,10 @@ def processCheck(self, trend=None): # type: (Optional[TrendingObject]) -> None args = (self.prepareTrendValues(trend),) if trend else () result = self.checkAlarm(*args) isAlarm, msg = result + msg = trend.name + ': ' + msg if isAlarm: - self._announceAlarm(msg) + alarmCollector.addAlarm([self, msg]) if self.parent: self.parent.childProcessed(child=self, result=isAlarm) diff --git a/overwatch/processing/alarms/collectors.py b/overwatch/processing/alarms/collectors.py index 1b393ab0..bafc2f84 100644 --- a/overwatch/processing/alarms/collectors.py +++ b/overwatch/processing/alarms/collectors.py @@ -19,11 +19,11 @@ class Singleton(_Singleton('SingletonMeta', (object,), {})): class Mail(Singleton): def __init__(self, alarmsParameters=None): if alarmsParameters is not None: - smtpSettings = alarmsParameters["emailDelivery"]["smtp_settings"] + smtpSettings = alarmsParameters["emailDelivery"]["smtpSettings"] host = smtpSettings["address"] port = smtpSettings["port"] password = smtpSettings["password"] - self.user_name = smtpSettings["user_name"] + self.user_name = smtpSettings["userName"] self.s = smtplib.SMTP(host=host, port=port) self._login(password) @@ -35,25 +35,45 @@ def printCollector(alarm): print(alarm) class MailSender: - """ADD DOC""" - def __init__(self, address): - self.address = address - self.mail = Mail() + """Manages sending emails. + + Args: + addresses (list): List of email addresses + Attributes: + recipients (list): List of email addresses + """ + def __init__(self, addresses): + self.recipients = addresses def __call__(self, alarm): self.sendMail(alarm) def sendMail(self, payload): - msg = MIMEMultipart() - msg['From'] = self.mail.user_name - msg['To'] = self.address - msg['Subject'] = 'Test Alarm' - msg.attach(MIMEText(payload, 'plain')) - printCollector("MAIL TO:{} FROM:alarm@overwatch PAYLOAD:'{}'".format(self.address, payload)) - self.mail.s.sendmail(self.mail.user_name, self.address, msg.as_string()) + """ Sends message to specified earlier recipients. + + Args: + payload (str): Message to send + Return: + None. + """ + if self.recipients is not None: + mail = Mail() + msg = MIMEMultipart() + msg['From'] = mail.user_name + msg['To'] = ", ".join(self.recipients) + msg['Subject'] = 'Overwatch Alarm' + msg.attach(MIMEText(payload, 'plain')) + mail.s.sendmail(mail.user_name, self.recipients, msg.as_string()) class SlackNotification(Singleton): - """ADD DOC""" + """Manages sending notifications on Slack. + + Args: + alarmsParameters (dict): Parameters read from configuration files + Attributes: + sc (SlackClient): + channel (str): Channel name + """ def __init__(self, alarmsParameters=None): if alarmsParameters is not None: self.sc = SlackClient(alarmsParameters["apiToken"]) @@ -63,6 +83,13 @@ def __call__(self, alarm): self.sendMessage(alarm) def sendMessage(self, payload): + """ Sends message to specified earlier channel. + + Args: + payload (str): Message to send + Return: + None. + """ self.sc.api_call('chat.postMessage', channel=self.channel, text=payload, username='Alarms OVERWATCH', icon_emoji=':robot_face:') @@ -70,3 +97,57 @@ def sendMessage(self, payload): def httpCollector(alarm): printCollector("HTTP: {}".format(alarm)) +class AlarmCollector(): + """ + Class that collects generated alarms. Collected alarms are grouped and announced to + specified receivers. + + Attributes: + alarms (list): List of alarms. Each element is a pair [Alarm, str] + """ + def __init__(self): + self.alarms = [] + + def addAlarm(self, alarm): + """ It adds alarm to the existing list of alarms + + Args: + alarm ([Alarm, str]): A pair - Alarm object and message + Return: + None. + """ + self.alarms.append(alarm) + + def announceAlarm(self): + """ It sends collected and grouped messages to receivers. + Then resets list of alarms. It can be called anywhere: + after processing each histogram, after each RUN, ect. + + Args: + None. + Return: + None. + """ + receivers = self._groupAlarms() + for receiver in receivers: + msg = '\n'.join(receivers[receiver]) + receiver(msg) + self._resetCollector() + + def _resetCollector(self): + self.alarms = [] + + def _groupAlarms(self): + receivers = {} + for alarmMsg in self.alarms: + alarm = alarmMsg[0] + msg = alarmMsg[1] + msg = "[{alarmText}]: {msg}".format(alarmText=alarm.alarmText, msg=msg) + for receiver in alarm.receivers: + if receiver not in receivers: + receivers[receiver] = [] + receivers[receiver].append(msg) + return receivers + + +alarmCollector = AlarmCollector() From 4d04717b589883f5820cc584e98c05133aa40869 Mon Sep 17 00:00:00 2001 From: arturro96 Date: Sun, 9 Dec 2018 15:29:57 +0100 Subject: [PATCH 21/38] added displaying alarms on webApp --- overwatch/processing/alarms/alarm.py | 2 +- overwatch/processing/alarms/example.py | 15 +++++---------- overwatch/processing/trending/manager.py | 5 +++++ overwatch/processing/trending/objects/object.py | 1 + 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/overwatch/processing/alarms/alarm.py b/overwatch/processing/alarms/alarm.py index 25f3bd5c..1da0ce24 100644 --- a/overwatch/processing/alarms/alarm.py +++ b/overwatch/processing/alarms/alarm.py @@ -27,9 +27,9 @@ def processCheck(self, trend=None): # type: (Optional[TrendingObject]) -> None args = (self.prepareTrendValues(trend),) if trend else () result = self.checkAlarm(*args) isAlarm, msg = result - msg = trend.name + ': ' + msg if isAlarm: + trend.alarmsMessages.append(msg) alarmCollector.addAlarm([self, msg]) if self.parent: self.parent.childProcessed(child=self, result=isAlarm) diff --git a/overwatch/processing/alarms/example.py b/overwatch/processing/alarms/example.py index 9c08ffc7..8a2784ab 100644 --- a/overwatch/processing/alarms/example.py +++ b/overwatch/processing/alarms/example.py @@ -1,6 +1,7 @@ -from overwatch.processing.alarms.collectors import workerMail, printCollector, httpCollector, MailSender +from overwatch.processing.alarms.collectors import printCollector, httpCollector, MailSender from overwatch.processing.alarms.impl.andAlarm import AndAlarm from overwatch.processing.alarms.impl.betweenValuesAlarm import BetweenValuesAlarm +from overwatch.processing.alarms.impl.checkLastNAlarm import CheckLastNAlarm class TrendingObjectMock: @@ -24,16 +25,10 @@ def alarmConfig(): boarderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") boarderWarning.addReceiver(printCollector) - borderError = BetweenValuesAlarm(minVal=0, maxVal=70, alarmText="ERROR") - borderError.receivers = [workerMail, httpCollector] + lastAlarm = CheckLastNAlarm(alarmText="ERROR") + lastAlarm.addReceiver(printCollector) - bva = BetweenValuesAlarm(minVal=0, maxVal=90, alarmText='BETWEEN') - # TODO add second alarm to andAlarm - seriousAlarm = AndAlarm([bva], "Serious Alarm") - cernBoss = MailSender("boss@cern") - seriousAlarm.addReceiver(cernBoss) - - return [boarderWarning, borderError, seriousAlarm] + return [boarderWarning, lastAlarm] def main(): diff --git a/overwatch/processing/trending/manager.py b/overwatch/processing/trending/manager.py index 93fb2fbe..0b0e3149 100644 --- a/overwatch/processing/trending/manager.py +++ b/overwatch/processing/trending/manager.py @@ -18,6 +18,8 @@ import overwatch.processing.pluginManager as pluginManager import overwatch.processing.trending.constants as CON from overwatch.processing.alarms.collectors import Mail, SlackNotification +from overwatch.processing.alarms.example import alarmConfig +from overwatch.processing.alarms.collectors import alarmCollector logger = logging.getLogger(__name__) @@ -115,6 +117,7 @@ def _createTrendingObjectFromInfo(self, subsystemName, infoList): for info in infoList: if info.name not in self.trendingDB[subsystemName] or self.parameters[CON.RECREATE]: to = info.createTrendingClass(subsystemName, self.parameters) + to.setAlarms(alarmConfig()) self.trendingDB[subsystemName][info.name] = to self._subscribe(to, info.histogramNames) @@ -166,3 +169,5 @@ def notifyAboutNewHistogramValue(self, hist): # type: (histogramContainer) -> N trend.extractTrendValue(hist) for alarm in trend.alarms: alarm.processCheck(trend) + hist.information[trend.name] = '\n'.join(trend.alarmsMessages) + alarmCollector.announceAlarm() diff --git a/overwatch/processing/trending/objects/object.py b/overwatch/processing/trending/objects/object.py index b1227771..927667af 100644 --- a/overwatch/processing/trending/objects/object.py +++ b/overwatch/processing/trending/objects/object.py @@ -39,6 +39,7 @@ def __init__(self, name, description, histogramNames, subsystemName, parameters) self.maxEntries = self.parameters.get(CON.ENTRIES, 100) self.trendedValues = self.initializeTrendingArray() self.alarms = [] + self.alarmsMessages = [] self.recipients = self.parameters["emailDelivery"]["recipients"][subsystemName] self.histogram = None From c5f531c9b44a8d7b12fa7c0265269ccd033bf41b Mon Sep 17 00:00:00 2001 From: arturro96 Date: Sun, 9 Dec 2018 17:03:53 +0100 Subject: [PATCH 22/38] WebApp displaying updates --- overwatch/processing/alarms/alarm.py | 1 + overwatch/processing/alarms/collectors.py | 4 ---- overwatch/processing/alarms/example.py | 2 +- .../processing/alarms/impl/absolutePreviousValueAlarm.py | 2 +- overwatch/processing/alarms/impl/betweenValuesAlarm.py | 2 +- overwatch/processing/alarms/impl/checkLastNAlarm.py | 2 +- overwatch/processing/alarms/impl/meanInRangeAlarm.py | 2 +- .../processing/alarms/impl/relativePreviousValueAlarm.py | 2 +- overwatch/processing/trending/manager.py | 6 ++++-- 9 files changed, 11 insertions(+), 12 deletions(-) diff --git a/overwatch/processing/alarms/alarm.py b/overwatch/processing/alarms/alarm.py index 1da0ce24..1a1372b1 100644 --- a/overwatch/processing/alarms/alarm.py +++ b/overwatch/processing/alarms/alarm.py @@ -27,6 +27,7 @@ def processCheck(self, trend=None): # type: (Optional[TrendingObject]) -> None args = (self.prepareTrendValues(trend),) if trend else () result = self.checkAlarm(*args) isAlarm, msg = result + msg = "[{alarmText}]: {msg}".format(alarmText=self.alarmText, msg=msg) if isAlarm: trend.alarmsMessages.append(msg) diff --git a/overwatch/processing/alarms/collectors.py b/overwatch/processing/alarms/collectors.py index bafc2f84..70760eb5 100644 --- a/overwatch/processing/alarms/collectors.py +++ b/overwatch/processing/alarms/collectors.py @@ -94,9 +94,6 @@ def sendMessage(self, payload): text=payload, username='Alarms OVERWATCH', icon_emoji=':robot_face:') -def httpCollector(alarm): - printCollector("HTTP: {}".format(alarm)) - class AlarmCollector(): """ Class that collects generated alarms. Collected alarms are grouped and announced to @@ -142,7 +139,6 @@ def _groupAlarms(self): for alarmMsg in self.alarms: alarm = alarmMsg[0] msg = alarmMsg[1] - msg = "[{alarmText}]: {msg}".format(alarmText=alarm.alarmText, msg=msg) for receiver in alarm.receivers: if receiver not in receivers: receivers[receiver] = [] diff --git a/overwatch/processing/alarms/example.py b/overwatch/processing/alarms/example.py index 8a2784ab..b99ae0c9 100644 --- a/overwatch/processing/alarms/example.py +++ b/overwatch/processing/alarms/example.py @@ -1,4 +1,4 @@ -from overwatch.processing.alarms.collectors import printCollector, httpCollector, MailSender +from overwatch.processing.alarms.collectors import printCollector, MailSender from overwatch.processing.alarms.impl.andAlarm import AndAlarm from overwatch.processing.alarms.impl.betweenValuesAlarm import BetweenValuesAlarm from overwatch.processing.alarms.impl.checkLastNAlarm import CheckLastNAlarm diff --git a/overwatch/processing/alarms/impl/absolutePreviousValueAlarm.py b/overwatch/processing/alarms/impl/absolutePreviousValueAlarm.py index 38dd8570..05948c72 100644 --- a/overwatch/processing/alarms/impl/absolutePreviousValueAlarm.py +++ b/overwatch/processing/alarms/impl/absolutePreviousValueAlarm.py @@ -21,6 +21,6 @@ def checkAlarm(self, trend): if delta <= self.maxDelta: return False, '' - msg = "curValue: {curValue}, prevValue: {prevValue}, change more than: {maxDelta}".format( + msg = "(AbsolutePreviousValueAlarm): curValue: {curValue}, prevValue: {prevValue}, change more than: {maxDelta}".format( curValue=curValue, prevValue=prevValue, maxDelta=self.maxDelta) return True, msg diff --git a/overwatch/processing/alarms/impl/betweenValuesAlarm.py b/overwatch/processing/alarms/impl/betweenValuesAlarm.py index 8e6ce486..59dae98b 100644 --- a/overwatch/processing/alarms/impl/betweenValuesAlarm.py +++ b/overwatch/processing/alarms/impl/betweenValuesAlarm.py @@ -17,5 +17,5 @@ def checkAlarm(self, trend): if self.minVal <= testedValue <= self.maxVal: return False, '' - msg = "value: {} not in [{}, {}]".format(testedValue, self.minVal, self.maxVal) + msg = "(BetweenValuesAlarm): value {} not in [{}, {}]".format(testedValue, self.minVal, self.maxVal) return True, msg diff --git a/overwatch/processing/alarms/impl/checkLastNAlarm.py b/overwatch/processing/alarms/impl/checkLastNAlarm.py index 05f06a80..afd99cf7 100644 --- a/overwatch/processing/alarms/impl/checkLastNAlarm.py +++ b/overwatch/processing/alarms/impl/checkLastNAlarm.py @@ -23,6 +23,6 @@ def checkAlarm(self, trend): if len(inBorderValues) >= self.ratio * self.N: return False, '' - msg = "less than {} % values of last: {} values not in {} {}".format( + msg = "(CheckLastNAlarm): less than {} % values of last {} trending values not in [{}, {}]".format( self.ratio * 10, self.N, self.minVal, self.maxVal) return True, msg diff --git a/overwatch/processing/alarms/impl/meanInRangeAlarm.py b/overwatch/processing/alarms/impl/meanInRangeAlarm.py index f3efb5d3..001ca835 100644 --- a/overwatch/processing/alarms/impl/meanInRangeAlarm.py +++ b/overwatch/processing/alarms/impl/meanInRangeAlarm.py @@ -23,6 +23,6 @@ def checkAlarm(self, trend): if self.minVal < np.mean(mean) < self.maxVal: return False, '' - msg = "mean value of last: {n} values not in {min} {max}".format( + msg = "(MeanInRangeAlarm): mean of last {n} values not in [{min}, {max}]".format( n=self.N, min=self.minVal, max=self.maxVal) return True, msg diff --git a/overwatch/processing/alarms/impl/relativePreviousValueAlarm.py b/overwatch/processing/alarms/impl/relativePreviousValueAlarm.py index 6d5073a7..9d4c5ee1 100644 --- a/overwatch/processing/alarms/impl/relativePreviousValueAlarm.py +++ b/overwatch/processing/alarms/impl/relativePreviousValueAlarm.py @@ -23,6 +23,6 @@ def checkAlarm(self, trend): if abs(prevValue / self.ratio) <= curValue <= abs(prevValue * self.ratio): return False, '' - msg = "curValue: {curValue}, prevValue: {prevValue}, change more than: {ratio}".format( + msg = "(RelativePreviousValueAlarm): curValue: {curValue}, prevValue: {prevValue}, change more than: {ratio}".format( curValue=curValue, prevValue=prevValue, ratio=self.ratio) return True, msg diff --git a/overwatch/processing/trending/manager.py b/overwatch/processing/trending/manager.py index 0b0e3149..abd65b43 100644 --- a/overwatch/processing/trending/manager.py +++ b/overwatch/processing/trending/manager.py @@ -169,5 +169,7 @@ def notifyAboutNewHistogramValue(self, hist): # type: (histogramContainer) -> N trend.extractTrendValue(hist) for alarm in trend.alarms: alarm.processCheck(trend) - hist.information[trend.name] = '\n'.join(trend.alarmsMessages) - alarmCollector.announceAlarm() + if trend.alarmsMessages: + hist.information[trend.name] = '; '.join(trend.alarmsMessages) + trend.alarmsMessages = [] + alarmCollector.announceAlarm() From 2cf02fdf5de6fd943ac9fc8f8d1f2b6eb1517626 Mon Sep 17 00:00:00 2001 From: arturro96 Date: Sun, 9 Dec 2018 18:19:19 +0100 Subject: [PATCH 23/38] Readme added --- overwatch/processing/alarms/README.md | 93 +++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 overwatch/processing/alarms/README.md diff --git a/overwatch/processing/alarms/README.md b/overwatch/processing/alarms/README.md new file mode 100644 index 00000000..8d8cf8b8 --- /dev/null +++ b/overwatch/processing/alarms/README.md @@ -0,0 +1,93 @@ + +# Alarms + +This module is responsible for generating alarms and sending notifications about them. + +Class Alarm has an abstract method checkAlarm(), which allows us to implement our own alarms. +Examples of alarms can be found in impl package. + +Alarms can be aggregated by logic functions or/and. + + +## Displaying on the webApp +When histogram is processed and alarms are generated, they are displayed above this histogram on the webApp. + +# Class Diagram +![Diagram](./doc/alarms_class_diag.png) + +# Notifications + +Each generated alarm is collected by AlarmCollector. It allows us send notifications about alarms when we want: +after processing trending object, after processing histogram or when all histograms are processed. You have to call +announceAlarm() method on alarmCollector object. AlarmCollector also groups alarms. + +## Emails + +There is possibility to send notifications about alarms via email. To send emails add to configuration file following information: + +```yaml +# Email configuration for gmail +emailDelivery: + smtpSettings: + address: "smtp.gmail.com" + port: 587 + userName: "email@address" + password: "password" + recipients: + EMC: + - "emcExpert1@mail" + - "emcExpert2@mail" + HLT: + - "hltExpert1@mail" + - "hltExpert2@mail" + TPC: + - "tpcExpert1@mail" + - "tpcExpert2@mail" +``` + +## Slack + +To send messages on Slack add to configuration file: + +```yaml +# Slack token +apiToken: 'token' + +# Slack channel +slackChannel: "test" +``` + +# Usage + +To specify alarms and receivers write following function: + +```python +def alarmConfig(recipients): + + mailSender = MailSender(recipients) + slackSender = SlackNotification() + boarderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") + + boarderWarning.receivers = [printCollector] + + borderError = BetweenValuesAlarm(minVal=0, maxVal=70, alarmText="ERROR") + borderError.receivers = [mailSender, slackSender] + + bva = BetweenValuesAlarm(minVal=0, maxVal=90, alarmText='BETWEEN') + # TODO add second alarm to andAlarm + seriousAlarm = AndAlarm([bva], "Serious Alarm") + seriousAlarm.addReceiver(mailSender) + + return [boarderWarning, borderError, seriousAlarm] +``` + +And in manager.py in update _createTrendingObjectFromInfo method: + +```python +for info in infoList: + if info.name not in self.trendingDB[subsystemName] or self.parameters[CON.RECREATE]: + to = info.createTrendingClass(subsystemName, self.parameters) + to.setAlarms(alarmConfig(to.recipients)) # Update here + self.trendingDB[subsystemName][info.name] = to + self._subscribe(to, info.histogramNames) +``` \ No newline at end of file From 77533762984dd358c274bd097f11f4f1968f075a Mon Sep 17 00:00:00 2001 From: arturro96 Date: Sun, 9 Dec 2018 18:23:17 +0100 Subject: [PATCH 24/38] Readme update --- overwatch/processing/alarms/doc/.gitignore | 1 + .../processing/alarms/doc/alarms_class_diag.png | Bin 0 -> 62199 bytes 2 files changed, 1 insertion(+) create mode 100644 overwatch/processing/alarms/doc/.gitignore create mode 100644 overwatch/processing/alarms/doc/alarms_class_diag.png diff --git a/overwatch/processing/alarms/doc/.gitignore b/overwatch/processing/alarms/doc/.gitignore new file mode 100644 index 00000000..74ed96ff --- /dev/null +++ b/overwatch/processing/alarms/doc/.gitignore @@ -0,0 +1 @@ +!*.png diff --git a/overwatch/processing/alarms/doc/alarms_class_diag.png b/overwatch/processing/alarms/doc/alarms_class_diag.png new file mode 100644 index 0000000000000000000000000000000000000000..38fdf6ba4ed46153a89993fc2a50401226730cce GIT binary patch literal 62199 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 Date: Sun, 9 Dec 2018 18:27:17 +0100 Subject: [PATCH 25/38] readme fix --- overwatch/processing/alarms/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/overwatch/processing/alarms/README.md b/overwatch/processing/alarms/README.md index 8d8cf8b8..b5ed5071 100644 --- a/overwatch/processing/alarms/README.md +++ b/overwatch/processing/alarms/README.md @@ -3,7 +3,7 @@ This module is responsible for generating alarms and sending notifications about them. -Class Alarm has an abstract method checkAlarm(), which allows us to implement our own alarms. +Class Alarm has an abstract method `checkAlarm()`, which allows us to implement our own alarms. Examples of alarms can be found in impl package. Alarms can be aggregated by logic functions or/and. @@ -19,7 +19,7 @@ When histogram is processed and alarms are generated, they are displayed above t Each generated alarm is collected by AlarmCollector. It allows us send notifications about alarms when we want: after processing trending object, after processing histogram or when all histograms are processed. You have to call -announceAlarm() method on alarmCollector object. AlarmCollector also groups alarms. +`announceAlarm()` method on alarmCollector object. AlarmCollector also groups alarms. ## Emails @@ -81,7 +81,7 @@ def alarmConfig(recipients): return [boarderWarning, borderError, seriousAlarm] ``` -And in manager.py in update _createTrendingObjectFromInfo method: +And in manager.py in update `_createTrendingObjectFromInfo` method: ```python for info in infoList: From 1deaa8d545f5d3abee335a383c0a3493f713cd39 Mon Sep 17 00:00:00 2001 From: arturro96 Date: Mon, 10 Dec 2018 13:50:59 +0100 Subject: [PATCH 26/38] Exceptions added and changed applying alarms, readme update --- overwatch/processing/alarms/README.md | 60 +++++++++++++---- overwatch/processing/alarms/collectors.py | 67 +++++++++++++------ overwatch/processing/alarms/example.py | 33 +++++++-- overwatch/processing/detectors/EMC.py | 15 ++++- overwatch/processing/trending/info.py | 11 +-- overwatch/processing/trending/manager.py | 2 - .../processing/trending/objects/object.py | 1 - 7 files changed, 142 insertions(+), 47 deletions(-) diff --git a/overwatch/processing/alarms/README.md b/overwatch/processing/alarms/README.md index b5ed5071..8da4df57 100644 --- a/overwatch/processing/alarms/README.md +++ b/overwatch/processing/alarms/README.md @@ -50,11 +50,11 @@ emailDelivery: To send messages on Slack add to configuration file: ```yaml -# Slack token -apiToken: 'token' - -# Slack channel -slackChannel: "test" +# Slack configuration +# Define token and channel +slack: + apiToken: 'token' + slackChannel: "alarms" ``` # Usage @@ -81,13 +81,49 @@ def alarmConfig(recipients): return [boarderWarning, borderError, seriousAlarm] ``` -And in manager.py in update `_createTrendingObjectFromInfo` method: +You can define separate alarms to different trending objects: + +```python +def alarmMeanConfig(): + slack = SlackNotification() + lastAlarm = CheckLastNAlarm(alarmText="ERROR") + lastAlarm.receivers = [printCollector, slack] + + return [lastAlarm] + +def alarmStdConfig(): + slack = SlackNotification() + meanInRangeWarning = MeanInRangeAlarm(alarmText="WARNING") + meanInRangeWarning.receivers = [printCollector, slack] + + return [meanInRangeWarning] + +def alarmMaxConfig(recipients): + mailSender = MailSender(recipients) + borderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") + borderWarning.receivers = [printCollector, mailSender] + + return [borderWarning] +``` + +To use alarms, define them in EMC.py or other detector file in `getTrendingObjectInfo()` function: ```python -for info in infoList: - if info.name not in self.trendingDB[subsystemName] or self.parameters[CON.RECREATE]: - to = info.createTrendingClass(subsystemName, self.parameters) - to.setAlarms(alarmConfig(to.recipients)) # Update here - self.trendingDB[subsystemName][info.name] = to - self._subscribe(to, info.histogramNames) + if "emailDelivery" in processingParameters: + recipients = processingParameters["emailDelivery"]["recipients"]["EMC"] + else: + recipients = None + alarms = { + "max": alarmMaxConfig(recipients), + "mean": alarmMeanConfig(), + # "stdDev": alarmStdConfig() + } + trendingInfo = [] + for prefix, cls in trendingNameToObject.items(): + for dependingFile, desc in infoList: + infoObject = TrendingInfo(prefix + dependingFile, desc, [dependingFile], cls) + if prefix in alarms: + infoObject.addAlarm(alarms[prefix]) + trendingInfo.append(infoObject) + return trendingInfo ``` \ No newline at end of file diff --git a/overwatch/processing/alarms/collectors.py b/overwatch/processing/alarms/collectors.py index 70760eb5..81a6abc0 100644 --- a/overwatch/processing/alarms/collectors.py +++ b/overwatch/processing/alarms/collectors.py @@ -2,6 +2,9 @@ import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText +import logging + +logger = logging.getLogger(__name__) # works in Python 2 & 3 class _Singleton(type): @@ -19,17 +22,21 @@ class Singleton(_Singleton('SingletonMeta', (object,), {})): class Mail(Singleton): def __init__(self, alarmsParameters=None): if alarmsParameters is not None: - smtpSettings = alarmsParameters["emailDelivery"]["smtpSettings"] - host = smtpSettings["address"] - port = smtpSettings["port"] - password = smtpSettings["password"] - self.user_name = smtpSettings["userName"] - self.s = smtplib.SMTP(host=host, port=port) - self._login(password) + try: + self.parameters = alarmsParameters + smtpSettings = alarmsParameters["emailDelivery"]["smtpSettings"] + host = smtpSettings["address"] + port = smtpSettings["port"] + password = smtpSettings["password"] + self.user_name = smtpSettings["userName"] + self.smtp = smtplib.SMTP(host=host, port=port) + self._login(password) + except KeyError: + logger.debug("EmailDelivery not configured") def _login(self, password): - self.s.starttls() - self.s.login(user=self.user_name, password=password) + self.smtp.starttls() + self.smtp.login(user=self.user_name, password=password) def printCollector(alarm): print(alarm) @@ -56,14 +63,22 @@ def sendMail(self, payload): Return: None. """ + success = "Emails successfully sent to {recipients}" + fail = "EmailDelivery not configured, couldn't send emails" + if self.recipients is not None: mail = Mail() - msg = MIMEMultipart() - msg['From'] = mail.user_name - msg['To'] = ", ".join(self.recipients) - msg['Subject'] = 'Overwatch Alarm' - msg.attach(MIMEText(payload, 'plain')) - mail.s.sendmail(mail.user_name, self.recipients, msg.as_string()) + if 'emailDelivery' in mail.parameters: + msg = MIMEMultipart() + msg['From'] = mail.user_name + msg['To'] = ", ".join(self.recipients) + msg['Subject'] = 'Overwatch Alarm' + msg.attach(MIMEText(payload, 'plain')) + mail.smtp.sendmail(mail.user_name, self.recipients, msg.as_string()) + + logger.debug(success.format(recipients=", ".join(self.recipients))) + else: + logger.debug(fail) class SlackNotification(Singleton): """Manages sending notifications on Slack. @@ -76,8 +91,13 @@ class SlackNotification(Singleton): """ def __init__(self, alarmsParameters=None): if alarmsParameters is not None: - self.sc = SlackClient(alarmsParameters["apiToken"]) - self.channel = alarmsParameters["slackChannel"] + self.parameters = alarmsParameters + # if 'slack' in self.parameters: + try: + self.sc = SlackClient(alarmsParameters["slack"]["apiToken"]) + self.channel = alarmsParameters["slack"]["slackChannel"] + except KeyError: + logger.debug("Slack not configured") def __call__(self, alarm): self.sendMessage(alarm) @@ -90,9 +110,16 @@ def sendMessage(self, payload): Return: None. """ - self.sc.api_call('chat.postMessage', channel=self.channel, - text=payload, username='Alarms OVERWATCH', - icon_emoji=':robot_face:') + success = "Message successfully sent on Slack channel {channel}" + fail = "Slack not configured, couldn't send messages" + + if 'slack' in self.parameters: + self.sc.api_call('chat.postMessage', channel=self.channel, + text=payload, username='Alarms OVERWATCH', + icon_emoji=':robot_face:') + logger.debug(success.format(channel=self.channel)) + else: + logger.debug(fail) class AlarmCollector(): """ diff --git a/overwatch/processing/alarms/example.py b/overwatch/processing/alarms/example.py index b99ae0c9..252f0eef 100644 --- a/overwatch/processing/alarms/example.py +++ b/overwatch/processing/alarms/example.py @@ -1,7 +1,9 @@ -from overwatch.processing.alarms.collectors import printCollector, MailSender +from overwatch.processing.alarms.collectors import printCollector, MailSender, SlackNotification from overwatch.processing.alarms.impl.andAlarm import AndAlarm from overwatch.processing.alarms.impl.betweenValuesAlarm import BetweenValuesAlarm from overwatch.processing.alarms.impl.checkLastNAlarm import CheckLastNAlarm +from overwatch.processing.alarms.impl.meanInRangeAlarm import MeanInRangeAlarm + class TrendingObjectMock: @@ -21,15 +23,34 @@ def __str__(self): return self.__class__.__name__ -def alarmConfig(): - boarderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") - boarderWarning.addReceiver(printCollector) +def alarmConfig(recipients): + mailSender = MailSender(recipients) + + borderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") + borderWarning.receivers = [printCollector, mailSender] + + return [borderWarning] +def alarmMeanConfig(): + slack = SlackNotification() lastAlarm = CheckLastNAlarm(alarmText="ERROR") - lastAlarm.addReceiver(printCollector) + lastAlarm.receivers = [printCollector, slack] + + return [lastAlarm] + +def alarmStdConfig(): + slack = SlackNotification() + meanInRangeWarning = MeanInRangeAlarm(alarmText="WARNING") + meanInRangeWarning.receivers = [printCollector, slack] + + return [meanInRangeWarning] - return [boarderWarning, lastAlarm] +def alarmMaxConfig(recipients): + mailSender = MailSender(recipients) + borderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") + borderWarning.receivers = [printCollector, mailSender] + return [borderWarning] def main(): to = TrendingObjectMock(alarmConfig()) diff --git a/overwatch/processing/detectors/EMC.py b/overwatch/processing/detectors/EMC.py index 3d595820..554e2dd0 100644 --- a/overwatch/processing/detectors/EMC.py +++ b/overwatch/processing/detectors/EMC.py @@ -33,6 +33,7 @@ from overwatch.processing.trending.info import TrendingInfo import overwatch.processing.trending.objects as trendingObjects +from overwatch.processing.alarms.example import alarmStdConfig, alarmMaxConfig, alarmMeanConfig def getTrendingObjectInfo(): """ Function create simple data objects - TrendingInfo, from which will be created TrendingObject. @@ -89,10 +90,22 @@ def getTrendingObjectInfo(): "mean": trendingObjects.MeanTrending, "stdDev": trendingObjects.StdDevTrending, } + if "emailDelivery" in processingParameters: + recipients = processingParameters["emailDelivery"]["recipients"]["EMC"] + else: + recipients = None + alarms = { + "max": alarmMaxConfig(recipients), + "mean": alarmMeanConfig(), + # "stdDev": alarmStdConfig() + } trendingInfo = [] for prefix, cls in trendingNameToObject.items(): for dependingFile, desc in infoList: - trendingInfo.append(TrendingInfo(prefix + dependingFile, desc, [dependingFile], cls)) + infoObject = TrendingInfo(prefix + dependingFile, desc, [dependingFile], cls) + if prefix in alarms: + infoObject.addAlarm(alarms[prefix]) + trendingInfo.append(infoObject) return trendingInfo def checkForEMCHistStack(subsystem, histName, skipList, selector): diff --git a/overwatch/processing/trending/info.py b/overwatch/processing/trending/info.py index 4bcc262b..4d5cb553 100644 --- a/overwatch/processing/trending/info.py +++ b/overwatch/processing/trending/info.py @@ -49,11 +49,12 @@ def __init__(self, name, desc, histogramNames, trendingClass): self._alarms = [] - def addAlarm(self, alarm): # type: (Alarm) -> None - if isinstance(alarm, Alarm): - self._alarms.append(alarm) - else: - raise TrendingInfoException(msg='WrongAlarmType') + def addAlarm(self, alarms): # type: ([Alarm]) -> None + for alarm in alarms: + if isinstance(alarm, Alarm): + self._alarms.append(alarm) + else: + raise TrendingInfoException(msg='WrongAlarmType') def createTrendingClass(self, subsystemName, parameters): # type: (str, dict) -> TrendingObject """Create instance of TrendingObject from previously set parameters diff --git a/overwatch/processing/trending/manager.py b/overwatch/processing/trending/manager.py index abd65b43..7cd24e8e 100644 --- a/overwatch/processing/trending/manager.py +++ b/overwatch/processing/trending/manager.py @@ -18,7 +18,6 @@ import overwatch.processing.pluginManager as pluginManager import overwatch.processing.trending.constants as CON from overwatch.processing.alarms.collectors import Mail, SlackNotification -from overwatch.processing.alarms.example import alarmConfig from overwatch.processing.alarms.collectors import alarmCollector logger = logging.getLogger(__name__) @@ -117,7 +116,6 @@ def _createTrendingObjectFromInfo(self, subsystemName, infoList): for info in infoList: if info.name not in self.trendingDB[subsystemName] or self.parameters[CON.RECREATE]: to = info.createTrendingClass(subsystemName, self.parameters) - to.setAlarms(alarmConfig()) self.trendingDB[subsystemName][info.name] = to self._subscribe(to, info.histogramNames) diff --git a/overwatch/processing/trending/objects/object.py b/overwatch/processing/trending/objects/object.py index 927667af..d381909a 100644 --- a/overwatch/processing/trending/objects/object.py +++ b/overwatch/processing/trending/objects/object.py @@ -40,7 +40,6 @@ def __init__(self, name, description, histogramNames, subsystemName, parameters) self.trendedValues = self.initializeTrendingArray() self.alarms = [] self.alarmsMessages = [] - self.recipients = self.parameters["emailDelivery"]["recipients"][subsystemName] self.histogram = None # Ensure that the axis and points are drawn on the TGraph From 5a62554cbdb11f68f15d7db4eebc52f31b912fef Mon Sep 17 00:00:00 2001 From: arturro96 Date: Mon, 10 Dec 2018 23:17:42 +0100 Subject: [PATCH 27/38] Small fixes --- overwatch/processing/alarms/README.md | 68 +++++++++++------- overwatch/processing/alarms/alarm.py | 2 +- overwatch/processing/alarms/collectors.py | 40 +++++++---- overwatch/processing/alarms/doc/absolute.png | Bin 0 -> 17731 bytes .../processing/alarms/doc/betweenAlarm.png | Bin 0 -> 22080 bytes .../processing/alarms/doc/meanRange1.png | Bin 0 -> 16040 bytes .../processing/alarms/doc/meanRange2.png | Bin 0 -> 16432 bytes .../processing/alarms/doc/meanRange3.png | Bin 0 -> 16608 bytes .../processing/alarms/doc/meanRange4.png | Bin 0 -> 16364 bytes overwatch/processing/alarms/example.py | 18 +++-- overwatch/processing/detectors/EMC.py | 11 ++- overwatch/processing/trending/info.py | 4 +- overwatch/processing/trending/manager.py | 2 +- .../webApp/templates/runPageMainContent.html | 13 +++- setup.py | 2 + 15 files changed, 107 insertions(+), 53 deletions(-) create mode 100644 overwatch/processing/alarms/doc/absolute.png create mode 100644 overwatch/processing/alarms/doc/betweenAlarm.png create mode 100644 overwatch/processing/alarms/doc/meanRange1.png create mode 100644 overwatch/processing/alarms/doc/meanRange2.png create mode 100644 overwatch/processing/alarms/doc/meanRange3.png create mode 100644 overwatch/processing/alarms/doc/meanRange4.png diff --git a/overwatch/processing/alarms/README.md b/overwatch/processing/alarms/README.md index 8da4df57..40720642 100644 --- a/overwatch/processing/alarms/README.md +++ b/overwatch/processing/alarms/README.md @@ -4,10 +4,33 @@ This module is responsible for generating alarms and sending notifications about them. Class Alarm has an abstract method `checkAlarm()`, which allows us to implement our own alarms. -Examples of alarms can be found in impl package. Alarms can be aggregated by logic functions or/and. +Examples of alarms can be found in impl package. + +## BetweenValuesAlarm + +Checks if trend is between minimal and maximal allowed values. + +![betweenAlarm example](./doc/betweenAlarm.png) + +## MeanInRangeAlarm + +Checks if mean from N last measurements is in the range. +![No alarm](./doc/meanRange1.png) +![No alarm](./doc/meanRange2.png) +![No alarm](./doc/meanRange3.png) +![Alarm](./doc/meanRange4.png) + +![betweenAlarm example](./doc/betweenAlarm.png) + +## AbsolutePreviousValueAlarm + +Check if (new value - old value) is different more than delta. + +![absolutePreviousValueAlarm example](./doc/absolute.png) + ## Displaying on the webApp When histogram is processed and alarms are generated, they are displayed above this histogram on the webApp. @@ -33,16 +56,6 @@ emailDelivery: port: 587 userName: "email@address" password: "password" - recipients: - EMC: - - "emcExpert1@mail" - - "emcExpert2@mail" - HLT: - - "hltExpert1@mail" - - "hltExpert2@mail" - TPC: - - "tpcExpert1@mail" - - "tpcExpert2@mail" ``` ## Slack @@ -59,16 +72,15 @@ slack: # Usage -To specify alarms and receivers write following function: +To specify alarms and receivers write for example following function: ```python def alarmConfig(recipients): - mailSender = MailSender(recipients) slackSender = SlackNotification() - boarderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") + borderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") - boarderWarning.receivers = [printCollector] + borderWarning.receivers = [printCollector] borderError = BetweenValuesAlarm(minVal=0, maxVal=70, alarmText="ERROR") borderError.receivers = [mailSender, slackSender] @@ -78,7 +90,7 @@ def alarmConfig(recipients): seriousAlarm = AndAlarm([bva], "Serious Alarm") seriousAlarm.addReceiver(mailSender) - return [boarderWarning, borderError, seriousAlarm] + return [borderWarning, borderError, seriousAlarm] ``` You can define separate alarms to different trending objects: @@ -106,24 +118,32 @@ def alarmMaxConfig(recipients): return [borderWarning] ``` -To use alarms, define them in EMC.py or other detector file in `getTrendingObjectInfo()` function: +To use alarms, define them in EMC.py or other detector file in `getTrendingObjectInfo()`: ```python - if "emailDelivery" in processingParameters: - recipients = processingParameters["emailDelivery"]["recipients"]["EMC"] - else: - recipients = None + trendingNameToObject = { + "max": trendingObjects.MaximumTrending, + "mean": trendingObjects.MeanTrending, + "stdDev": trendingObjects.StdDevTrending, + } + + # Add email recipients + recipients = { + "max": ["test1@mail", "test2@mail"] + } + + # Assign created earlier alarms to particular trending objects alarms = { - "max": alarmMaxConfig(recipients), + "max": alarmMaxConfig(recipients["max"]), "mean": alarmMeanConfig(), - # "stdDev": alarmStdConfig() + "stdDev": alarmStdConfig() } trendingInfo = [] for prefix, cls in trendingNameToObject.items(): for dependingFile, desc in infoList: infoObject = TrendingInfo(prefix + dependingFile, desc, [dependingFile], cls) if prefix in alarms: - infoObject.addAlarm(alarms[prefix]) + infoObject.addAlarm(alarms[prefix]) # Set alarms trendingInfo.append(infoObject) return trendingInfo ``` \ No newline at end of file diff --git a/overwatch/processing/alarms/alarm.py b/overwatch/processing/alarms/alarm.py index 1a1372b1..17f5af0b 100644 --- a/overwatch/processing/alarms/alarm.py +++ b/overwatch/processing/alarms/alarm.py @@ -31,7 +31,7 @@ def processCheck(self, trend=None): # type: (Optional[TrendingObject]) -> None if isAlarm: trend.alarmsMessages.append(msg) - alarmCollector.addAlarm([self, msg]) + alarmCollector.addAlarm(self, msg) if self.parent: self.parent.childProcessed(child=self, result=isAlarm) diff --git a/overwatch/processing/alarms/collectors.py b/overwatch/processing/alarms/collectors.py index 81a6abc0..f1e881ed 100644 --- a/overwatch/processing/alarms/collectors.py +++ b/overwatch/processing/alarms/collectors.py @@ -6,41 +6,48 @@ logger = logging.getLogger(__name__) + # works in Python 2 & 3 class _Singleton(type): """ A metaclass that creates a Singleton base class when called. """ _instances = {} + def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] + class Singleton(_Singleton('SingletonMeta', (object,), {})): # https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python pass + class Mail(Singleton): def __init__(self, alarmsParameters=None): if alarmsParameters is not None: + self.parameters = alarmsParameters try: - self.parameters = alarmsParameters smtpSettings = alarmsParameters["emailDelivery"]["smtpSettings"] host = smtpSettings["address"] port = smtpSettings["port"] password = smtpSettings["password"] self.user_name = smtpSettings["userName"] - self.smtp = smtplib.SMTP(host=host, port=port) - self._login(password) except KeyError: logger.debug("EmailDelivery not configured") + else: + self.smtp = smtplib.SMTP(host=host, port=port) + self._login(password) def _login(self, password): self.smtp.starttls() self.smtp.login(user=self.user_name, password=password) + def printCollector(alarm): print(alarm) + class MailSender: """Manages sending emails. @@ -49,6 +56,7 @@ class MailSender: Attributes: recipients (list): List of email addresses """ + def __init__(self, addresses): self.recipients = addresses @@ -80,28 +88,29 @@ def sendMail(self, payload): else: logger.debug(fail) + class SlackNotification(Singleton): """Manages sending notifications on Slack. Args: alarmsParameters (dict): Parameters read from configuration files Attributes: - sc (SlackClient): + slackClient (SlackClient): channel (str): Channel name """ + def __init__(self, alarmsParameters=None): if alarmsParameters is not None: self.parameters = alarmsParameters - # if 'slack' in self.parameters: try: - self.sc = SlackClient(alarmsParameters["slack"]["apiToken"]) + self.slackClient = SlackClient(alarmsParameters["slack"]["apiToken"]) self.channel = alarmsParameters["slack"]["slackChannel"] except KeyError: logger.debug("Slack not configured") def __call__(self, alarm): self.sendMessage(alarm) - + def sendMessage(self, payload): """ Sends message to specified earlier channel. @@ -114,14 +123,15 @@ def sendMessage(self, payload): fail = "Slack not configured, couldn't send messages" if 'slack' in self.parameters: - self.sc.api_call('chat.postMessage', channel=self.channel, - text=payload, username='Alarms OVERWATCH', - icon_emoji=':robot_face:') + self.slackClient.api_call('chat.postMessage', channel=self.channel, + text=payload, username='Alarms OVERWATCH', + icon_emoji=':robot_face:') logger.debug(success.format(channel=self.channel)) else: logger.debug(fail) -class AlarmCollector(): + +class AlarmCollector(object): """ Class that collects generated alarms. Collected alarms are grouped and announced to specified receivers. @@ -129,18 +139,20 @@ class AlarmCollector(): Attributes: alarms (list): List of alarms. Each element is a pair [Alarm, str] """ + def __init__(self): self.alarms = [] - def addAlarm(self, alarm): + def addAlarm(self, alarm, msg): """ It adds alarm to the existing list of alarms Args: - alarm ([Alarm, str]): A pair - Alarm object and message + alarm (Alarm): Alarm object + msg (str): generated message Return: None. """ - self.alarms.append(alarm) + self.alarms.append((alarm, msg)) def announceAlarm(self): """ It sends collected and grouped messages to receivers. diff --git a/overwatch/processing/alarms/doc/absolute.png b/overwatch/processing/alarms/doc/absolute.png new file mode 100644 index 0000000000000000000000000000000000000000..6226499a19b252c7971bdb308bf1faad5bd14e9b GIT binary patch literal 17731 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 literal 0 HcmV?d00001 diff --git a/overwatch/processing/alarms/doc/betweenAlarm.png b/overwatch/processing/alarms/doc/betweenAlarm.png new file mode 100644 index 0000000000000000000000000000000000000000..fdc41fb6a6b286e4357696123ef21b793f8f49c5 GIT binary patch literal 22080 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 LnpY8u7J>f{2s>w{ literal 0 HcmV?d00001 diff --git a/overwatch/processing/alarms/doc/meanRange1.png b/overwatch/processing/alarms/doc/meanRange1.png new file mode 100644 index 0000000000000000000000000000000000000000..5888b1d06132024ad05f247fc6bf89dabbcd0098 GIT binary patch literal 16040 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 J^Dfxk`CpF+%EbTx literal 0 HcmV?d00001 diff --git a/overwatch/processing/alarms/doc/meanRange2.png b/overwatch/processing/alarms/doc/meanRange2.png new file mode 100644 index 0000000000000000000000000000000000000000..b92816ad4e4f12486f94467ae899c5f056a4db69 GIT binary patch literal 16432 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 Bk3|3g literal 0 HcmV?d00001 diff --git a/overwatch/processing/alarms/doc/meanRange3.png b/overwatch/processing/alarms/doc/meanRange3.png new file mode 100644 index 0000000000000000000000000000000000000000..2dae81b11863ccf640c5b7ed68d399b1784edf27 GIT binary patch literal 16608 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 None + def addAlarm(self, alarms): # type: (Union(Alarm, List[Alarm])) -> None + if not isinstance(alarms, list): + alarms = [alarms] for alarm in alarms: if isinstance(alarm, Alarm): self._alarms.append(alarm) diff --git a/overwatch/processing/trending/manager.py b/overwatch/processing/trending/manager.py index 7cd24e8e..39602fed 100644 --- a/overwatch/processing/trending/manager.py +++ b/overwatch/processing/trending/manager.py @@ -168,6 +168,6 @@ def notifyAboutNewHistogramValue(self, hist): # type: (histogramContainer) -> N for alarm in trend.alarms: alarm.processCheck(trend) if trend.alarmsMessages: - hist.information[trend.name] = '; '.join(trend.alarmsMessages) + hist.information[trend.name] = '\n'.join(trend.alarmsMessages) trend.alarmsMessages = [] alarmCollector.announceAlarm() diff --git a/overwatch/webApp/templates/runPageMainContent.html b/overwatch/webApp/templates/runPageMainContent.html index 3994c3a6..37c19c63 100644 --- a/overwatch/webApp/templates/runPageMainContent.html +++ b/overwatch/webApp/templates/runPageMainContent.html @@ -45,8 +45,17 @@

{{ hist.prettyName }}

{{ label }}
- -
{{ info }}
+ + {%- if label not in ("Threshold", "Fast OR Hot Channels ID")-%} +
+ {% for infoData in info.split('\n') %} +
{{ infoData }}
+ {% endfor %} +
+ {% else %} +
{{ info }}
+ {% endif -%}

{% endfor -%} diff --git a/setup.py b/setup.py index ea523b70..7c9eff32 100644 --- a/setup.py +++ b/setup.py @@ -122,6 +122,8 @@ def getVersion(): "uwsgi", # Flask monitoring "sentry-sdk[flask]", + # Notifications + "slackclient", ], # Include additional files From aef471228deac17fa6673e06f5a4b851aa1c80e0 Mon Sep 17 00:00:00 2001 From: arturro96 Date: Tue, 11 Dec 2018 01:42:28 +0100 Subject: [PATCH 28/38] AlarmCollector fix --- overwatch/processing/alarms/README.md | 2 +- overwatch/processing/alarms/alarm.py | 2 +- overwatch/processing/alarms/collectors.py | 61 +++++++++++-------- overwatch/processing/alarms/example.py | 5 +- overwatch/processing/trending/manager.py | 4 +- .../processing/trending/objects/stdDev.py | 2 +- 6 files changed, 44 insertions(+), 32 deletions(-) diff --git a/overwatch/processing/alarms/README.md b/overwatch/processing/alarms/README.md index 40720642..5695ab38 100644 --- a/overwatch/processing/alarms/README.md +++ b/overwatch/processing/alarms/README.md @@ -42,7 +42,7 @@ When histogram is processed and alarms are generated, they are displayed above t Each generated alarm is collected by AlarmCollector. It allows us send notifications about alarms when we want: after processing trending object, after processing histogram or when all histograms are processed. You have to call -`announceAlarm()` method on alarmCollector object. AlarmCollector also groups alarms. +`announceAlarm()` method on alarmCollector object. To print messages on console call `showOnConsole()` method. ## Emails diff --git a/overwatch/processing/alarms/alarm.py b/overwatch/processing/alarms/alarm.py index 17f5af0b..955661dc 100644 --- a/overwatch/processing/alarms/alarm.py +++ b/overwatch/processing/alarms/alarm.py @@ -31,7 +31,7 @@ def processCheck(self, trend=None): # type: (Optional[TrendingObject]) -> None if isAlarm: trend.alarmsMessages.append(msg) - alarmCollector.addAlarm(self, msg) + alarmCollector.collectMessage(self, msg) if self.parent: self.parent.childProcessed(child=self, result=isAlarm) diff --git a/overwatch/processing/alarms/collectors.py b/overwatch/processing/alarms/collectors.py index f1e881ed..26df23a4 100644 --- a/overwatch/processing/alarms/collectors.py +++ b/overwatch/processing/alarms/collectors.py @@ -1,8 +1,14 @@ +""" Alarms collectors. + +.. code-author: Artur Wolak , AGH University of Science and Technology +""" + from slackclient import SlackClient import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText import logging +from collections import defaultdict logger = logging.getLogger(__name__) @@ -133,18 +139,18 @@ def sendMessage(self, payload): class AlarmCollector(object): """ - Class that collects generated alarms. Collected alarms are grouped and announced to + Class that collects generated messages from alarms. Collected messages are grouped and announced to specified receivers. Attributes: - alarms (list): List of alarms. Each element is a pair [Alarm, str] + receivers (dict): Dictionary where key is receiver and value is generated messages form alarms """ def __init__(self): - self.alarms = [] + self.receivers = defaultdict(list) - def addAlarm(self, alarm, msg): - """ It adds alarm to the existing list of alarms + def collectMessage(self, alarm, msg): + """ It collects and assigns message to receivers. Args: alarm (Alarm): Alarm object @@ -152,10 +158,13 @@ def addAlarm(self, alarm, msg): Return: None. """ - self.alarms.append((alarm, msg)) + for receiver in alarm.receivers: + if receiver not in self.receivers: + self.receivers[receiver] = [] + self.receivers[receiver].append(msg) def announceAlarm(self): - """ It sends collected and grouped messages to receivers. + """ It sends collected messages to receivers if it's not printCollector. Then resets list of alarms. It can be called anywhere: after processing each histogram, after each RUN, ect. @@ -164,25 +173,25 @@ def announceAlarm(self): Return: None. """ - receivers = self._groupAlarms() - for receiver in receivers: - msg = '\n'.join(receivers[receiver]) - receiver(msg) - self._resetCollector() - - def _resetCollector(self): - self.alarms = [] - - def _groupAlarms(self): - receivers = {} - for alarmMsg in self.alarms: - alarm = alarmMsg[0] - msg = alarmMsg[1] - for receiver in alarm.receivers: - if receiver not in receivers: - receivers[receiver] = [] - receivers[receiver].append(msg) - return receivers + for receiver in self.receivers.keys(): + if receiver != printCollector: + msg = '\n'.join(self.receivers[receiver]) + receiver(msg) + self.receivers.pop(receiver) + + def showOnConsole(self): + """ Prints generated messages on console. + Can be called anywhere. + + Args: + None. + Return: + None. + """ + if printCollector in self.receivers: + msg = self.receivers.pop(printCollector) + msg = '\n'.join(msg) + printCollector(msg) alarmCollector = AlarmCollector() diff --git a/overwatch/processing/alarms/example.py b/overwatch/processing/alarms/example.py index d087dace..b65ff501 100644 --- a/overwatch/processing/alarms/example.py +++ b/overwatch/processing/alarms/example.py @@ -45,7 +45,10 @@ def alarmMeanConfig(): lastAlarm = CheckLastNAlarm(alarmText="ERROR") lastAlarm.receivers = [printCollector, slack] - return [lastAlarm] + borderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") + borderWarning.receivers = [printCollector] + + return [lastAlarm, borderWarning] def alarmStdConfig(): slack = SlackNotification() diff --git a/overwatch/processing/trending/manager.py b/overwatch/processing/trending/manager.py index 39602fed..f8ace115 100644 --- a/overwatch/processing/trending/manager.py +++ b/overwatch/processing/trending/manager.py @@ -5,7 +5,7 @@ notify appropriate objects about new histograms, manage processing trending histograms. .. code-author: Pawel Ostrowski , AGH University of Science and Technology -.. code-author: Artur Wolak <>, AGH University of Science and Technology +.. code-author: Artur Wolak , AGH University of Science and Technology """ import logging import os @@ -170,4 +170,4 @@ def notifyAboutNewHistogramValue(self, hist): # type: (histogramContainer) -> N if trend.alarmsMessages: hist.information[trend.name] = '\n'.join(trend.alarmsMessages) trend.alarmsMessages = [] - alarmCollector.announceAlarm() + alarmCollector.showOnConsole() diff --git a/overwatch/processing/trending/objects/stdDev.py b/overwatch/processing/trending/objects/stdDev.py index d94686d7..9151ea99 100644 --- a/overwatch/processing/trending/objects/stdDev.py +++ b/overwatch/processing/trending/objects/stdDev.py @@ -3,7 +3,7 @@ """ Trending object to extract the standard deviation of a histogram. .. code-author: Pawel Ostrowski , AGH University of Science and Technology -.. code-author: Artur Wolak <>, AGH University of Science and Technology +.. code-author: Artur Wolak , AGH University of Science and Technology """ import numpy as np From f51ce6580d1a719b0196f281294efd0ec4b9b119 Mon Sep 17 00:00:00 2001 From: arturro96 Date: Tue, 11 Dec 2018 13:23:53 +0100 Subject: [PATCH 29/38] Update checkLastNAlarm --- overwatch/processing/alarms/impl/checkLastNAlarm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/overwatch/processing/alarms/impl/checkLastNAlarm.py b/overwatch/processing/alarms/impl/checkLastNAlarm.py index afd99cf7..b21cccf5 100644 --- a/overwatch/processing/alarms/impl/checkLastNAlarm.py +++ b/overwatch/processing/alarms/impl/checkLastNAlarm.py @@ -24,5 +24,5 @@ def checkAlarm(self, trend): return False, '' msg = "(CheckLastNAlarm): less than {} % values of last {} trending values not in [{}, {}]".format( - self.ratio * 10, self.N, self.minVal, self.maxVal) + self.ratio * 100, self.N, self.minVal, self.maxVal) return True, msg From 6ecea7044f3e6d1193367b8a16db666f955c458a Mon Sep 17 00:00:00 2001 From: arturro96 Date: Tue, 11 Dec 2018 15:23:04 +0100 Subject: [PATCH 30/38] Deleted recipients Recipients moved to example.py --- overwatch/processing/detectors/EMC.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/overwatch/processing/detectors/EMC.py b/overwatch/processing/detectors/EMC.py index 165a384b..67a10243 100644 --- a/overwatch/processing/detectors/EMC.py +++ b/overwatch/processing/detectors/EMC.py @@ -90,11 +90,8 @@ def getTrendingObjectInfo(): "mean": trendingObjects.MeanTrending, "stdDev": trendingObjects.StdDevTrending, } - recipients = { - "max": ["test1@mail", "test2@mail"] - } alarms = { - "max": alarmMaxConfig(recipients["max"]), + "max": alarmMaxConfig(), "mean": alarmMeanConfig(), "stdDev": alarmStdConfig() } From eea61973aeabd1f12329c23d7f120a2661bceb00 Mon Sep 17 00:00:00 2001 From: arturro96 Date: Tue, 11 Dec 2018 15:24:45 +0100 Subject: [PATCH 31/38] Update example.py --- overwatch/processing/alarms/example.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/overwatch/processing/alarms/example.py b/overwatch/processing/alarms/example.py index b65ff501..9c09ecf4 100644 --- a/overwatch/processing/alarms/example.py +++ b/overwatch/processing/alarms/example.py @@ -23,7 +23,9 @@ def __str__(self): return self.__class__.__name__ -def alarmConfig(recipients): +def alarmConfig(): + recipients = ["test1@mail", "test2@mail"] + mailSender = MailSender(recipients) slackSender = SlackNotification() borderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") @@ -57,7 +59,8 @@ def alarmStdConfig(): return [meanInRangeWarning] -def alarmMaxConfig(recipients): +def alarmMaxConfig(): + recipients = ["test1@mail", "test2@mail"] mailSender = MailSender(recipients) borderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") borderWarning.receivers = [printCollector, mailSender] @@ -65,8 +68,7 @@ def alarmMaxConfig(recipients): return [borderWarning] def main(): - recipients = ["test1@mail", "test2@mail"] - to = TrendingObjectMock(alarmConfig(recipients)) + to = TrendingObjectMock(alarmConfig()) values = [3, 14, 15, 92, 65, 35, 89, 79] for i, val in enumerate(values): From 10afa10461d66f5ae0bc3040775765c45619fc6b Mon Sep 17 00:00:00 2001 From: Nabywaniec Date: Wed, 12 Dec 2018 18:32:47 +0100 Subject: [PATCH 32/38] Update test_alarms.py --- tests/processing/alarms/test_alarms.py | 53 +++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/processing/alarms/test_alarms.py b/tests/processing/alarms/test_alarms.py index 3371588c..4587fe28 100644 --- a/tests/processing/alarms/test_alarms.py +++ b/tests/processing/alarms/test_alarms.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ Tests for alarm implementations. - .. code-author: Pawel Ostrowski , AGH University of Science and Technology +.. code-author: Jacek Nabywaniec , AGH University of Science and Technology """ import pytest @@ -88,3 +88,54 @@ def test(val, isAlarm=False): test(-17, True) test(-15) test(-9, True) + + +def testMeanInRangeAlarm(af_alarmChecker, af_trendingObjectClass): + alarm = MeanInRangeAlarm(minVal=0, maxVal=10) + alarm.addReceiver(af_alarmChecker.receiver) + to = af_trendingObjectClass() + to.alarms = [alarm] + + def test(val, isAlarm=False): + af_alarmChecker.addValueAndCheck(to, val, isAlarm) + + test(10) + test(3) + test(-1) + test(2) + test(5) + test(30) + test(20, True) + test(1, True) + test(-10) + test(100, True) + test(0, True) + test(0, True) + test(-10, True) + test(-2, True) + test(20) + + +def testCheckLastNAlarm(af_alarmChecker, af_trendingObjectClass): + alarm = CheckLastNAlarm(minVal=0, maxVal=10, ratio=0.6) + alarm.addReceiver(af_alarmChecker.receiver) + to = af_trendingObjectClass() + to.alarms = [alarm] + + def test(val, isAlarm=False): + af_alarmChecker.addValueAndCheck(to, val, isAlarm) + + test(10) + test(8) + test(4) + test(2) + test(-20) + test(-10) + test(12, True) + test(1, True) + test(1, True) + test(2) + test(0) + test(-2) + test(-4, True) + test(10, True) From c5de074acfad0511f1095baf2894b5e07372e7d1 Mon Sep 17 00:00:00 2001 From: arturro96 Date: Wed, 12 Dec 2018 22:40:21 +0100 Subject: [PATCH 33/38] Collector update and small fixes --- overwatch/processing/alarms/alarm.py | 2 +- overwatch/processing/alarms/collectors.py | 23 +++++++++++++++---- overwatch/processing/alarms/example.py | 21 +++++++++-------- .../processing/alarms/impl/checkLastNAlarm.py | 2 +- overwatch/processing/detectors/EMC.py | 5 +--- 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/overwatch/processing/alarms/alarm.py b/overwatch/processing/alarms/alarm.py index 955661dc..8e31125a 100644 --- a/overwatch/processing/alarms/alarm.py +++ b/overwatch/processing/alarms/alarm.py @@ -31,7 +31,7 @@ def processCheck(self, trend=None): # type: (Optional[TrendingObject]) -> None if isAlarm: trend.alarmsMessages.append(msg) - alarmCollector.collectMessage(self, msg) + alarmCollector.collectMessage(self, "[{}]".format(trend.name) + msg) if self.parent: self.parent.childProcessed(child=self, result=isAlarm) diff --git a/overwatch/processing/alarms/collectors.py b/overwatch/processing/alarms/collectors.py index 26df23a4..a735e7d0 100644 --- a/overwatch/processing/alarms/collectors.py +++ b/overwatch/processing/alarms/collectors.py @@ -163,10 +163,9 @@ def collectMessage(self, alarm, msg): self.receivers[receiver] = [] self.receivers[receiver].append(msg) - def announceAlarm(self): - """ It sends collected messages to receivers if it's not printCollector. - Then resets list of alarms. It can be called anywhere: - after processing each histogram, after each RUN, ect. + def announceOnEmail(self): + """ It sends emails with collected messages to recipients defined in configuration. + It can be called anywhere: after processing each histogram, after each RUN, ect. Args: None. @@ -174,11 +173,25 @@ def announceAlarm(self): None. """ for receiver in self.receivers.keys(): - if receiver != printCollector: + if receiver != printCollector and receiver != SlackNotification(): msg = '\n'.join(self.receivers[receiver]) receiver(msg) self.receivers.pop(receiver) + def announceOnSlack(self): + """ It sends collected messages on Slack. + Can be called anywhere. + + Args: + None. + Return: + None. + """ + if SlackNotification() in self.receivers: + msg = self.receivers.pop(SlackNotification()) + msg = '\n'.join(msg) + SlackNotification()(msg) + def showOnConsole(self): """ Prints generated messages on console. Can be called anywhere. diff --git a/overwatch/processing/alarms/example.py b/overwatch/processing/alarms/example.py index b65ff501..ed44802d 100644 --- a/overwatch/processing/alarms/example.py +++ b/overwatch/processing/alarms/example.py @@ -23,7 +23,8 @@ def __str__(self): return self.__class__.__name__ -def alarmConfig(recipients): +def alarmConfig(): + recipients = ["test1@mail", "test2@mail"] mailSender = MailSender(recipients) slackSender = SlackNotification() borderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") @@ -42,11 +43,13 @@ def alarmConfig(recipients): def alarmMeanConfig(): slack = SlackNotification() + recipients = ["test@mail"] + mailSender = MailSender(recipients) lastAlarm = CheckLastNAlarm(alarmText="ERROR") - lastAlarm.receivers = [printCollector, slack] + lastAlarm.receivers = [printCollector, slack, mailSender] borderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") - borderWarning.receivers = [printCollector] + borderWarning.receivers = [printCollector, mailSender] return [lastAlarm, borderWarning] @@ -57,16 +60,16 @@ def alarmStdConfig(): return [meanInRangeWarning] -def alarmMaxConfig(recipients): +def alarmMaxConfig(): + recipients = ["test@mail"] mailSender = MailSender(recipients) - borderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") - borderWarning.receivers = [printCollector, mailSender] + borderError = BetweenValuesAlarm(minVal=0, maxVal=300, alarmText="ERROR") + borderError.receivers = [printCollector] - return [borderWarning] + return [borderError] def main(): - recipients = ["test1@mail", "test2@mail"] - to = TrendingObjectMock(alarmConfig(recipients)) + to = TrendingObjectMock(alarmConfig()) values = [3, 14, 15, 92, 65, 35, 89, 79] for i, val in enumerate(values): diff --git a/overwatch/processing/alarms/impl/checkLastNAlarm.py b/overwatch/processing/alarms/impl/checkLastNAlarm.py index afd99cf7..b21cccf5 100644 --- a/overwatch/processing/alarms/impl/checkLastNAlarm.py +++ b/overwatch/processing/alarms/impl/checkLastNAlarm.py @@ -24,5 +24,5 @@ def checkAlarm(self, trend): return False, '' msg = "(CheckLastNAlarm): less than {} % values of last {} trending values not in [{}, {}]".format( - self.ratio * 10, self.N, self.minVal, self.maxVal) + self.ratio * 100, self.N, self.minVal, self.maxVal) return True, msg diff --git a/overwatch/processing/detectors/EMC.py b/overwatch/processing/detectors/EMC.py index 165a384b..67a10243 100644 --- a/overwatch/processing/detectors/EMC.py +++ b/overwatch/processing/detectors/EMC.py @@ -90,11 +90,8 @@ def getTrendingObjectInfo(): "mean": trendingObjects.MeanTrending, "stdDev": trendingObjects.StdDevTrending, } - recipients = { - "max": ["test1@mail", "test2@mail"] - } alarms = { - "max": alarmMaxConfig(recipients["max"]), + "max": alarmMaxConfig(), "mean": alarmMeanConfig(), "stdDev": alarmStdConfig() } From a4ae4e83b9861d0b75cc0eb76cde9a06fd5b3b9b Mon Sep 17 00:00:00 2001 From: arturro96 Date: Wed, 12 Dec 2018 22:55:40 +0100 Subject: [PATCH 34/38] Readme update --- overwatch/processing/alarms/README.md | 41 ++++++++++++------ overwatch/processing/alarms/doc/last.png | Bin 0 -> 15116 bytes overwatch/processing/alarms/doc/last1.png | Bin 0 -> 15174 bytes overwatch/processing/alarms/doc/last2.png | Bin 0 -> 15607 bytes overwatch/processing/alarms/doc/last3.png | Bin 0 -> 16205 bytes overwatch/processing/alarms/doc/relative.png | Bin 0 -> 15858 bytes overwatch/processing/alarms/doc/relative1.png | Bin 0 -> 16057 bytes overwatch/processing/alarms/doc/relative2.png | Bin 0 -> 16209 bytes overwatch/processing/alarms/doc/relative3.png | Bin 0 -> 16203 bytes 9 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 overwatch/processing/alarms/doc/last.png create mode 100644 overwatch/processing/alarms/doc/last1.png create mode 100644 overwatch/processing/alarms/doc/last2.png create mode 100644 overwatch/processing/alarms/doc/last3.png create mode 100644 overwatch/processing/alarms/doc/relative.png create mode 100644 overwatch/processing/alarms/doc/relative1.png create mode 100644 overwatch/processing/alarms/doc/relative2.png create mode 100644 overwatch/processing/alarms/doc/relative3.png diff --git a/overwatch/processing/alarms/README.md b/overwatch/processing/alarms/README.md index 5695ab38..f6fbbf76 100644 --- a/overwatch/processing/alarms/README.md +++ b/overwatch/processing/alarms/README.md @@ -23,14 +23,31 @@ Checks if mean from N last measurements is in the range. ![No alarm](./doc/meanRange3.png) ![Alarm](./doc/meanRange4.png) -![betweenAlarm example](./doc/betweenAlarm.png) - ## AbsolutePreviousValueAlarm Check if (new value - old value) is different more than delta. ![absolutePreviousValueAlarm example](./doc/absolute.png) +## RelativeProviousValueAlarm + +Check if new value is between (previous value)/ratio and (previous value)*ratio. + +![No alarm](./doc/relative.png) +![Alarm](./doc/relative1.png) +![No alarm](./doc/relative2.png) +![Alarm](./doc/relative3.png) + + +## CheckLastNAlarm + +Check if minimum ratio*N last N alarms are in range. + +![No alarm](./doc/last.png) +![No alarm](./doc/last1.png) +![Alarm](./doc/last2.png) +![No alarm](./doc/last3.png) + ## Displaying on the webApp When histogram is processed and alarms are generated, they are displayed above this histogram on the webApp. @@ -41,8 +58,10 @@ When histogram is processed and alarms are generated, they are displayed above t # Notifications Each generated alarm is collected by AlarmCollector. It allows us send notifications about alarms when we want: -after processing trending object, after processing histogram or when all histograms are processed. You have to call -`announceAlarm()` method on alarmCollector object. To print messages on console call `showOnConsole()` method. +after processing trending object, after processing histogram or when all histograms are processed. To send messages via email +you have to call +`announceOnEmail()` method on alarmCollector object. To print messages on console call `showOnConsole()` method. To +send on Slack call `announceOnSlack()` ## Emails @@ -75,7 +94,8 @@ slack: To specify alarms and receivers write for example following function: ```python -def alarmConfig(recipients): +def alarmConfig(): + recipients = ["test1@mail", "test2@mail"] mailSender = MailSender(recipients) slackSender = SlackNotification() borderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") @@ -110,7 +130,8 @@ def alarmStdConfig(): return [meanInRangeWarning] -def alarmMaxConfig(recipients): +def alarmMaxConfig(): + recipients = ["test1@mail", "test2@mail"] mailSender = MailSender(recipients) borderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") borderWarning.receivers = [printCollector, mailSender] @@ -126,15 +147,9 @@ To use alarms, define them in EMC.py or other detector file in `getTrendingObjec "mean": trendingObjects.MeanTrending, "stdDev": trendingObjects.StdDevTrending, } - - # Add email recipients - recipients = { - "max": ["test1@mail", "test2@mail"] - } - # Assign created earlier alarms to particular trending objects alarms = { - "max": alarmMaxConfig(recipients["max"]), + "max": alarmMaxConfig(), "mean": alarmMeanConfig(), "stdDev": alarmStdConfig() } diff --git a/overwatch/processing/alarms/doc/last.png b/overwatch/processing/alarms/doc/last.png new file mode 100644 index 0000000000000000000000000000000000000000..683d71c4b5afb443654eb86da08a09479fae57c4 GIT binary patch literal 15116 zcmeHuc_5Vi`ZuDqA}z`qMY2o?*{2lBR#8#58v8PXu``x>iV`Zjv9*!1@4Klm*<~Ay z!Gp2Sh{0gOdr!=Cp68t3?>XnZ?;r2^y{~_ad%pMe-LBbf;1?tOhUP_v>{gyB;Ipxvp^=-Bu8txE<|KK~5_aE8 z(#y#i7{$P#?4<~Nbh2{0C*bAe2!$(psR;fYp$L4YJ_ZR2{2b!updx6bdrd$c=4vG% zD|t@xtf1;10RaJJS4(Tf8<#Y;UkCnD5wvx4b5;a_JUu-nJ!K?et~MYk1qB7rS!s~8 zv;;6h0`3iUyXPeVg$r#7p~<;q1&6rWIlI}xpaN96_wK{o-BbhxsT0wEw$|xpXZ>p? zD13Wczy?9oZ$MI#XF+t?z^lsCr;7Tnc2>a5RQak>%0EZ`;o0^)${_0Gf3ujao&J0Z zY*lrSGKhX`s(W-kix@L7DBQbp>4KpbBLUX}9jh-pJDi;8xF73_jr;iZ%&y}Sk#o{a z%_}zXmn~o?o$iBLjn_nWx*nZ4seMK3RvGITaAgUCg*4@qf-z8!#94;o#BZrF`)C~r zt^1JvKKb@L`{l@3wx;@vfeegH%&hDJ;8czvbJ>mqEdpTLquj&n!{r>*?|wc>ILJOM z*PV1+o%X!|%AJk&CF{9U0w|{sO0th>^1y*Jd~{j1SHXc;Lr?pC+mi(bGGJ8b6SYSL zGL&QxySTPi0?22~_NC1NP8DTjgc{&7q<$`w`k2XtOq(STY{0}ckXCL0p|6vrgyZu5RpjTh3?lK{I+vRZ3{2g@oV(-f%iY_06}>m^^)>HZ8GJO&L!Dz} zB%TA6(FQP|Im$kKYw*GcO`2Yx)lJP{Ag`_YP=3!{3G6MfY4bnf;^|qU%dfZ*%6XEm zu$g+p4PSyY5?0niS#9U$=H!naJ&M%}As97|<0Cl)^@rvOyV*7`RWDTPlx**hwdrpJ;ymroO$W$CQqu@ zBDS>X67O)*toNKido$cw#whGtA6HfjN>&s4Ca00Gb4`zvZYn;^3A>#kA3ovncn_#k zYfhNx-e5@ub8cYbbM*YvVL1&Kwj>A=uWYT7B{oLe z0ehT!>JtX}hrK_*#wg1>dN)G~7rbWg(kJZav=v0*x+SDOAhq>OPm`NQdGr;1}%dvlwyra2{hyAx@iBlVE&cqpG>ZkTr|{vgR&=6Tp+uVlKo zTf{^eUqa%+o~^S1w=mt0*%-7FMzxH)YdXtRnP?{S^htEWu`MtEJVp{lfQ=U#$~V#P zJ;;i=R(zeP+x&?DOE9E)w;=e>smFngKg*{`ek>C2%vZ;IT>deo>^`W2jc+; z*Qlhm1nj;2u-80h$Srb~vsdhUtlAF;7~e70LP3iIP%)JZUixj`WfUrfG6U~t8lpl5 zt|}30yX8g#?mNeRyoh3j9>%2}qz{Dy9&qBNVQVUCeSLjcitha2dx7UiZVuiWu9&OF z`YkkwAWMe)eJNQkc^fOE5%MpfG@r?101ouk!O2J{J?K3OGIOln+@Q3usr|^k8L||i zwo&T6@uN(DtTVT_W$SL!fiQAF3w=~&)@OgB@al!(P*8PmC3(;nnNz(sEgJ!<`Qb`h zoa$_uND0`uQ_j1645&ol7#LetUI&}z1a+QNW4$DR-4Epy4zHW(4fU=;AGxK<$drEQ zd$Pw>Cyf0CAlzI8-RgQA_xO3|!N(tDh1=T8Dmq#yxObymj7$!k-;;}r`3!~C_R-?_c_46b>3+ZFz!zT`60i8)wP)%`MGKe@%N*X3Nn1^#SZqDo*kc495i@)lBs*on7qNN zaDjcrz!CEmataPriw(2 z$%zYkncW|pJHB>-cQ?CHN+7v#X(|3VM&$d<+K0CqS-X{emuDw+pL$nGy}qvZ62JlVbQnwqMFd?Oa6$&V;pUBenNVW4~D|j{^rI&FJ&UxK4aM$(p%j=<;+U3v-Fs9JpHq zJS@vNXP4vBdm{HXIItpCYbV_TWdXIvblsi4N_QFm&WIJ^f}p{Evekj)M@s+u@d0pX zRn^slwTXSNZZBW(@4h!i$~v2IpXSwUwNsxclyYQheD60U&dr%?mseH}5Y0_Y97l|p z%g6hxH7tW^5y~gXddC2(mr!a|RTa+TMtE-wqZ4zf)EYNvFD*cUxtVlzu20T9WrgNs zXZt5VWfv-za=j~=7XXJF2=~$buu&N}k`WsIbzG5hTGItSltA~NDnY_&r`;+}X;?uP zm!-6I%)MrB=mmQ@8n=qjVk(zNyGbDOO(E_Z)`cg>b+?|maj)R^mzh^m%S87`VLWUX zi|4NFb(j`+r^n)6P`e@UUa`Vctdl=E?;e6*c_tZ>LYvyclu4JRRN*4_=hVm!XT9vt-RSr4zWdzaVIsxC=vjZgn1&BU z!yD7>{!2o;NPE|zN7Xx*-|dD{4Bv8WN5X#g^GysTVcDH~=z>&((g2Vk`@OvoVZN9P zQ>=Z3asLGZMgjM-!=I^_k*i_b;|4Xd=v;qa`Oq+ezL^97P9($*&|Pvg;CjX81iFQ2 zrx+(JfJz!aB~QP)zljK2W6}V1PWNu0sfxh0GQd|Y9JrxEU#A^Y@ei<7j# zPK6`~+@6DcxB_43Y#YLR6V#zc+tKEqm(4U_XXZ*1%W5bofXez;w>Yd&y2Gq`@*abR z^!7eeT>%IM-nIHV_%fejdmI2>-gufPTwr^Y=)%%Ag;uR+kSYH9a5nO*V^FilNO zPHZNCeFN5B5(HI<^%$C8a!)xIBvkvT}v~N18O|asZ!Xhy1$6n|sg6U%r<;7{ZoGxU&#?(gW}EjArx0i&QWJ z{}aqhJ8k&K1OXhV)S?ZgA1)BsN~1lWPt%TrRfm&(Sda7)NfS%87$~0itq*+zoqz-C zto((l-|OAF>%%=Us|lkE;AinE<1b)Q5iGzWf0t|HmI+YW&Z9kdtIxkyBAoky0U0oTB1e+`{SbD{A{P zwKRWf0WXh?+|P}b1c6G>f^7hDmur{Bcsy^SMF`Xx79bxJvIs-T?!ZA|V7s9aJasLjkRJUF zN&wn2k_t9NIK4J{^kWw`+@u$7ntxr-pC)3>S2x#E*1cQLYH|vd;zJ0)kFc47;$1#u zN6qTJBGHMj^gx|k2?oYUN#AYF+TX+K2oEQmxBxAw#8+J15BQVsk*Ls`jdMlQc>YcP zgG|0G10dfgw~|iWPio;e&o1AOe(wjoU14#*|Dt3tbGdi6Y>+7v9#Jfo0@>1k)I8N2 zjf%2jhb(+5?|--`i4!=v3}lLJ2`H{00C+=54I(LygZ>nP?vKMKdiVo=%nNsHdn1%9 zlP+`V>hF0*Xdro(mf&YRLUM&PtP_38B%SfHdoHn4M(RRa$U^}&tG@^H7wi5_w&NL= z^r4lyhW2M*8Nlo2N(pkCkF)3WhNqvlOy!>GjX*!BvxQ|gjkYYlf1G&fk10^m3}O`@ zi%i!~fvRr)AYnQP+v#CCr`N*wdmqxQh0&8v;>UWSo{F1MLX}DCSR890H)P5otx%J- z*6lZ*GJ@E6i~P`lcjX+99drGj%9R;dvGL|Ya6p*Kb>{o7DmjZqWQsU1JWZF!LRF!o%$vG=R?t5b-UVBDYG)42%pzS z7Jll;Pm=QN!$1*ubKrZ{pPu$?BruP#R^6|h`~(1J-tc!lpt)HKVE~xuJby~dfZ5-x z171~A?L7S$c@7NV(QJ)^h>? z$C}D18^jQV-pzW8Z=$5DZwNugjYsOM_EoMgW05QQ(MEt4qmf!_^{&%1yl?h5o^+N3 zf(?Aij&PEi5PfQN;Q5Gsvf(>FQa4VSs~Pgw^*AR`ElEN12Mj0&Z1^n;kh?H}mO)&*kwpZEP>kg_3t~pt(#>esa-N*YWU?rw`8v@tr%uda3pV?GbEJ))>fH((k z=#A|8v`YZk;W60&jNaOpXuk11=V6l(<%tyU2@S{Kn8`1u=-^iFt7=P2kEdO<$|-pE zqNPM3faVetDqWYnv76o2NI+wC?E%)-l_wN4&{U@T%tGi^QQL*p!hYZ-a}zJj??C0GsxibUtH+9XE89^HF)5jn0i0R?Tj~Qr_V@8sZUfMq3?wS1L1xL{I4vgI=RMd#s_E4M9O>Dx z$K~T#ei?gdVSR{rf-%+<1k`n1CfG3`ZFVzIIX&^#72P7v>KY$0jWs-mrwP_eus>ap4a8%7h;y@I~6pJzBOr54^4t>lw!oBm-n@ z$qoCR8`UC`ciWL#l<~n)%|nTE9>-4#)x6~UYNqs! z=Pzrdn|-ypIDm4H9l#6fr}i<(kH4+u$#rc|xaX3|Jr@1uri)(ly-=Im8s(2rWxK_f zPP{WvEv&(-$_fs;&e?M_GC`OV_BjWup7ikcE^FA8j8-Hi`4rmgpuehHTnT!|kL_M( zak~;KYlaP3Iqs{Q1Sp0+Ld{aoUF_UhPz2uN znPuHB=@I?(BTPlCi?}v;w}e{Y;ukixeMjQGcgmc<77gBbV1DzRA+;Q(dC1nDSw2nR zQhPb8Q^hWKizQp3gj>(pdM!*NuYEwclp$$Y%QAx=eA~Au(%2R$qs@AbW4Jz` z8XtfHe(!1({NAz{SN+`X1z(PlYf?nllk`)yFm;NmSFy?}D!^40?>kpTCb^IJaJ;1k z6>h`5u57hV-wXZjf6DfiCY|f`)7>J1B3TmR9xA1Eb4;3Zkalt5Hxz9#(m3>TF!tLf3o`?W*@=GIwN8H}k`ZaL93lll*7W?z~ zjag?88ZG3oS4`spPMWGr7f`H3FsL>$h8mYNPElgsU)PbQrF#dc#CfiJO>}03nsfhq zOOmLjRKulh-sZd(VDs$QQGMx@h55%$oro_F&PSd zAIYi7NRvDRWJy5@%m7oDA>XyFbil=bW76l-IAg9_9#y|5WNNq3@fpLGEfBbWPl)lF zQa3BKCiGXCCwdR7^X-0@55M>mkN^r`?thWaHvlkkRY|XiP9m%U5&kEb*}lMAXQ;dw z@5Ddi?a;sNr&l!psq}oi-pC2J8C{y~D==>#!r`1cgTpsRn0Nm~!&F`R$Z0$N(Fb6T zYifq5dGcRZ8aaloJuTU}DR__B z*f$k<{MygP1_uUy6potH)kc^(!Ar-8xj;dX`%S@oBdTbhTN5F#iab`c>FIcDb&G8R z>OF^7ayAtROSfE$fP!Dg+QP!Z1THb{JN)x+VTm~vE~7fvBBQsIUI0io;f&DMJPAz~ zws;$)(B~%47sqKJe~qPhhiiaeU%>Zg2FSRF$j*cL{E#yjcNV#b$GR$YX3{UH|Ddff zN+Ml&pZj&y#F*CEOMRZDia?d%^ zWj2o7@kq}67xO0y_E`#LA=7=8+eqzWQMud0?2oV)ERywr^ieKH=>c*IuedPA3X4z1 zLSz0s2G~}BO!~WW<1{)2EG`5TfIVUN4?qJt-_oct*2Fhpc-yC>kDOHG8#*yY?4VOp zxrsp5gEC`K`e$J|KZeA8cYLMXWNuF5~NJ3tNpz@J&Ff1~F9*L>4oQ)h8%R!U-T;s-JnZjnWedNYtUQSM@f8!xXJtgQ-6B@NW%|C3=NQ-rg+$4j9ZRi$ z4;ANNKp&h!pi6eG)7y-4eS!e*0yHvIR{A*=%J~RuItslHBXE_4RhEsthwc0lPb_)$ zXPe-R*c}Qk;(lu_;Op+b1aE>LB2O@L)4f~^z_DL`H5(rn!!A^wH+JaJ!b%)rC2TS; zM+)?Gj!Am^EdbwWIyMEf6 z($P2yE@VHtpBvn57y)ztMfViC88{zhg98_Xz z(YYSiaf*X|Zja-Bvi`TI*e&O0bQRp(0Sbs=gsRnj1R4~?_TJq(3UC#*y`kFf?;Qmv z+&SW#%RKkD>^g%Z9#iKvwtCs^E{G) zyu9pzRa!O5CHf#t)9dN32*G3nl>3rX*&{eY(GT!%KHVRRJiG?o3#92k9ZAsA9%b*9 zv)xTgw>EPV&?W<>K~BmSe&1(&;EUUlEd-w%2RM3m9W7dlD8>ks z`X-5pnj~L6B10mr!3^~o;D>__u>Y%fK0M1bps%`WNtqgqgHTd7S5pc++N!-P)Q>7H zd8;q=##UcaVXGRiF5bthw&;_t3VXjn>CiWAQ_m^M;Z;YoRSjmeaevsXUNpdv+Ghw3 z=juV@S{`dDNuh3b_hx# zr)>V1ko&rhR@;~l)$EUwk6taESQt%B+75Gjfz0heNvy;|TYYz&;l^pXD)6$kE2=SWor`oNAhQn}p4^Beq|^ZeLR>Olk?09p{j zPCe-LTOz<@)`!%r;t`lzbeM!!K^N2A4Mi}M`ydvbJ; z`AHMOH_iE@lM(226ERY_XhG&}NB;6C{9<>2j%k{iuX#LfG%sK>{}LrX`*(#dc^AiU za$M=HFeh=!_q;h7*UVCfh165-#0*$(Vb@lbFkjS}fZG6j2WI7t_GT4Q0 zU)HVCix04;X?W%Ml5_jQ+3}|5`htm;4fDx4pUz`4#(ZW#=7f2JWG{^uu8#ubh%I8aGo>GGtpt@8wzMl&${pX1rM*U#YbrFy_B;4ey^hnpllIJ7_s_Qxai^lNQpCil4CN zbyaJe=_#nJ&n6JPjXjb(v$Wr)d_Win8+p#$+x&97M zP?#Mgj4)Hd&qnxisy}u;(mi}RO1l^aoayP3ak=H93LWkOQwY$pVWWm#d;YF|8Lwh8 z_{PB}qG8QuXy5w2nDuDyrgPY)J-|YX9Ft0Jw{hqGV0^m6B@*A#yZ$L+6whY7q4~nj z`=PZD5j}6xxX@JC9BGBmGa|o>MpKG0UDb=z%a_e78Vm1W1kg@jWCi4Di3lnF0@Ew?#>cWM@R|MH2DV5i)kXYaVm)idPlKgFC#m;56qbuF& zsGflvt&gUVIh~l{U7uUNuyroCx3si)D0T2b`&)oo`QWPT3y~Njv;tnb_yAs^qBz#k z+(gNw@zjirjZ#5#X=|_R#PpX^>*m#`)y=+w9pff3j^=}J@Rh~A$#CO*L|;r|$uY8i z;pz!+Le>+sqj{Hn|8v6REk#O>4nIZaMsfknw?VGHC}Lf;z>@M=b}-u=lhhF8alZtj5@o+*pt5Yj3(|%x5*I5W89hH>TEN|YV18m%9o@9p zj9fY$p^Iyki?YId@P$eTXDf77f8|#j4&KS8jzRWUe@!8L^|*WoI+3TvU2ff1|CO_5 z>=HQ2sy3pFo3x2ZCw~yiL;u#18F6UH^@b~ zeDq;Aoy0OZk><>mY8Wfc;HnbDyb0n_X2fslPAnYZTTylU)1ciSa~Uy46Nz zQ!i<4!Ahwy-&HAMDX9b`G3_-;(a9o&`eZGsSepmn=ZmV*mg}Bbqt(fyD%KcT9^aq) zb@ZtY!f%|I@#AcTlj>X6PhpCRK^tGFJ&hf;rzaqrJA5!b( zh;X0D|JaN`=MO;Ram$xepTA+_0FQZ78t3?i+|9f*@1{T%43-OkZ~N+XBGzqf!g=as z@B3bDM`ETwntO}q^u@yL?@%RUKl%<{+t6xrSi*}&=!zEPWJ*gk5n_ysK_xP< zO!$W@9C?|2%wsjJXFWmy*8HWx%rsqGD93nb zxwrC6GzHRQY~~FGH0AU}7okZ$10CM4fh*wc%hqZF?%pol5n>{rCLERXUR~TEUEGp8 zU-!BORj-t+C?h^uLvOy-6JHW>i>8!5O?+8d5_;qZWN|3lr=>fp*rQY9-gA@IFX+Qcx3pHPquGi{l<&~>}@o;_Zr`n$Pq8Dpu2>H@9KwA zJbYH!9i6c3*!jj&9gxl6bs@+@GtvP>n++rM{Au}EU>h$c#{7F;A=R3`k_ryQ!~(aD?`Ym58Zh3|Q~gq9?IiZaR9`Z5)173sftXo*3N(78%oAF_sCie3tM$APAx-#? zN4WE}t=0L*yqLIjM-t)Gta#}d?sl@g2k%7eQcFJRMXOadAvtA+W zj=P7RBENwe#2yE{exvWW6ahJXV$&;V$`iYium4geAP&wef2V8=3FB+ymR=aklMWbb z;&0Sy<1P`GhY`gK0DH77^R5&Jh3gyVKQo!DxMiXgo2f}eD>jPG$P^)rBdXm?pm=vr zC(owom`K7`KQt6llHE6iTg;;v&zWG_tF!I82<@k^(KE+^2o>Z0)V;(Eg1sB_?Y17q zHJlvVe@)D%LXPI0Rz>uyeF&5|@z@Mcc24#GF*xO{gt zx5Lo<-~;kWq8NN=UZrl+?|ft5?VjpUwWD(PM!BpGn5t$M&G}Hm*`lk&*14>fO+ncO zb9X4=ljR?p7L`-H;ScGgN zsc0@erE~Fox6d<5T=_Fmk%dH`V+J_}n1&_)gI)(e1`mP7>A4qk0=p3*xH+L6v(@`S zrF8lL3ceun%yU|DX>kQ0>0hp8)8XCO3V0piJAU_W?|& zvrhpswWllOwhJzSs96AV_gy(mgXRLcZvmNC5Ewe1i!%lCBc%)`I++x6h<#WxWA4{~ zClvra3^pP+1?Vzg0y0l6nbNxfEj)mQ&%WJra;uLQs9xUyx?+HTPM~*UHdA{5k_?0B r=b;T$EMmJno#+eypL`g@n>$E^Y811z88h|&R>B+>*z2`xZ?$S5i$7J63)upmec zHQEglz{XS%Gp5@zww;&%>B;!p6A|kp6egF-TSTUUFEla>&=7920FWU z9Nxjf!Lj?|g>zRqI6%c59Gsn8TY)zjJ&9j|e>hQBb>EPQC}BlBudp+U2LO;D)7vF4G|OFXyWOlE@q;C zSyao_!&X#L=9G-An8prKQBkPJEj!h#=d?Gw1OHPObMW+ZQ-wf$e0*el6l7dI>>+Y0 zDk>0Jd5FBcG|)mCg+_Yb^p!@U#J>;18t0rX3gO}C=IQ8)6lIQk)5g`yQ(a7qIT8EM z_jP(Y+WnXbiQ3#2ut5m(3q(#v7Q!AI=n7@NRWTi|ge^9W9eRIAm~wEaBwjps=9(`jy_bX>swt5jNcq+L!+f6-?PT7i zv`_L&GU8R?K{2M_dF|F?^IwwEgKR{7+zs-ge!CSNCcCTJpJFVV+Si0uS3K3FU(`8n zGpCgndMno?%kSYR`ub}-P4v<`XDKijk0>mdgA)Wk%)5AAoZhkv#C~PY$(b#HWqxBe zN;|>Hi8ShcCCB~_vRGh!42BtjKs_0yMhNzJ;NpqRp1~a03^2G@-z&3*H5nI|TkkLI zf%La?xh0MoL>&3P5-uK|fzoa4ffDxe3@Ck0KCHE|OyIR>l@~XApi@Uht6W~IDc)l( z1r|IeytySV9*0Y?V9GV0+q+mx3FaWHvt|)(i3;YxznW-2#NIP!wm)l@|G!~Q(Eo@N z9x&D~w9BZ0qNi_VJlV(6lGE8$#8?Q+Ltdjl=&36+a@Y(bPvUn9vIlS!ucb2D2F~qq{&AFHs}2;fsf@IqClj8Pg>w2Ic~g# zjWBX+-eX}3HR|rf6qKy>)aZ|d@3MJm3TtfbS0jOoyUHiHkQuRKbx+01Z2gdBrdNM% zv!!^IloT^+7tcO-x5J!rx|RNXC^tspv1pdG2L!hyEIkh`QG*1EcNbRAxJ$ElJqLKg zZOi94)HuZBIk5Q$e5c6pR4<0&nk=?*eO)wjFWL-9(ex*qSuDPxg+vedWvj zfOK9TkQFobTDWGUBEI(7^JCZbYOaYcuad2-ZFd98Thv==rn9N#r`0$)fd)&{y9t$* z@U`dJ7ruW;dK=8K;9Ry=ynB73Y*HyTV7i`0$M^RII$30Ct}PDg|8CBjzjhzbfUwbm z9x+mM{$tp|#L3F_z?l!+n$G)eRtIsK$P~>WnL710#@YOh1FE_TSOVwB;`30;9BkXK znp^=>9}gxj^!dBad^Uzx(59%daAb-GrgUj#rWvk_XBl*y5iD3OjUPVyZcNcFVQ3qc z>sHx&mp`g=eTDqxCKo#YXpn$eU&N;Ox5W_pp2TE(2?O)Yd>#I1#^1){VfIJM`Pw0V zRUhM0bY+>Rpg7Fh1*BVB@R4srH&aH4GY+GRH)=wO@4^>GY2lB9ea+4Wq-7fmGm=}uk<#DByYdlm8?_u|?L!SGc+sEY4)eaKJpj0mtn;E8 zZ{7|0cej$wbv0iO`)vsy*fZOk4ME>W#FTsfthZgA%DSp{bkv1U7S?nhkqrT6-g@pl8)meNi&nkOgtY(0GGgnkfZ^=A|B&TQ zcwXwjg3S%#FSoONRMjp(uKYav&#(f@Wn8eUyujBm44gJ#TKq4FD1ZwjlgWv33Tm?r z8g)6s;Q@s7qt~-oe$Zw6DYkq~;B*BS;PH5hK?5&EJ}rVnSvzRZmJP}rw9+1&*nzR+eNX@V`Lm%BuT8a*Hj;RB z-W7>#_?5!;VUhM>~up#6{-2DMGjtL;gXv->f& zBo_n#JpmRbTi^H2m6qzeumlCZ2Tn)` zDRVlfbf+(_lt%#+?!mdz!KVER{KR#JnYVc!(WnhHo zE#xY~srN3a4^M5zPM845o`0a|lb0D=30fpt+V|ZhY!yx@nhX=3*AG2CTiV2i4ZcZ$ z-c=cX_y6A&>2gW%i5<~Ck4AY}s;0-sGhi_DB$1_xX&wMFrwaY<$CgGL0Jq!9hmNt( z$V&@AH__W(3A1(c6rjDQASG-GoIJ=gaHH?cYgSwkmDR68xos;C`1FB|n}Rn$40g}- zdG23i*>EU&XWaA4|LoX+D<|n9*$AUy3%K~7-Xe6HPZjZ?`mU-AtbJaS5e1;0D126y z89x8_h|l3MUYl%uiqT!XV z8cI@asNO(hrz4wf2B6@C<>#S`mrVlWwLIaC^(2^bN3Mv-IU@Iq=-TENWra^RRjGx* zM7f^V1CD2@-7)vR8QxJNIelxuTo}*J8Qujl-Ne^OIqX;*bZpX0nq{*|nJ@*ETXR06 zU=vsZ3ALnX75!nS1j|TG3SlUteYDYt#j5V?qm>tIjD>4i7z-Q&*yo00znc?rxn-j3+2RCz9|!)Q-s14U9$x@oa~`T^MGmHa z`9)|bOnB@6F7W|r$p!#WreJz|dso}H%q7Ky4bH)U1=<10Ti(Hg2SX}60S}>m z06j4|iN|J_6opvqVFQ2@EqVX|Z~|xZfA#hk*KsBWD{1>9hLo+CKyITIRtcOC|NOGI z4*XFfr&~M(^d&JdQNCV@9a9>%B=EnF2&{A5)+GUCojDOrwBtx&k^I2v{&lruiBMpi z(>t-luB(~xeFDwT%S3uFED&F8UpV*;zMiky%ujQ$0pkU8D=I2_t0i^pi3p=*Wz8%l zsvw#jrVJo0)m+H%#u$`Sh$4uRgz&%ch$1~XYni^4IeJA^Smm0%8JVcLR@r8(z5De+ zw@SgvFtLuVl_}~Jq##f7l-!*%A~ZvK$m2p<;;$~o-E>Du2F%Wy&;w&YzHOXA z6|b_fLQ0kNTc&33v_MCIc&iD%Q#LcJ3;nKvi+_f?@cl!6WQoXpt-zGzDe;Yia1O7l ziB+kVEE+tqga2!d>il4?wOqHr*tD;L`s*WMKP64x6_CmmkD1w#Hqvlv(3)AP(Dy^L zR>4Yei)7rsZRZi*SMvl*_s*O#RQZFVQqUX0Q7jiju)E~{8%$59?lFa6(-$y9O0H!z zJ=V6~4T7vKkGUwWPlpD*eO5Z^Ys0xfoKgc)Vb`8&qkRn-^c~I${c+HEastd)G~>wl z4y1Sx4L$aD-QU3h&$Agssy=}efK>IA&=$V0y!StKVVF)){k*Q&caLYsfFd2Qy*06 zh+qnq<|(eqA_3ptJ4r_l%qO}Mx~knbc>*B)=A3Zsu>?^5Wcf@JZ7ipP^GV7He~hA3TW%cFd!L9@tm2+ObskutulpX$Y}V^ciE?IM;i7(WWFzB zgVP>3>GgR7iCccRIsZ{Yb>7ruN9 zBQ5j?RjwwCJHP@g=W&{A#ptz_rSUTpK!#@-`cf9#0CbG@xK4HHzF{f?hq((O zEZWTU`A@nKmPg-^W#;~x;0KL2wUYgvG0BDlJzG+sQ0W0`iMS_41jqHE^?I%$Kus;? zdxx)hg}iY1bDK!OJM;`mSveI*R6iXSG!m1Mk%_#qCbu&XAWB@r)`=o(UX8?YX|)XT z>ihJTy_n*$#TK;Ih%VgqrLdE34mHp;krK81&~&*ld|xuj zsTqrmb5i@OYrl@0yH*tD@2nZ>GKIOj>6s#?_nq(l6*f=@=mY0K+;b*((S7m>P9-mI z2S+s22`h)Ki-Q{RG~ZmKwVqjFk_pKMtz>Qyz(E{~Y%Y<)RH%$)OM!-ui&m*WwSwQi zMb$g4h@ygYw@GHk?alQ(C@oYSH2X;>(Y11Q-u?^{jPD1`_}_I6v4Z>EqVBq&I8_I# zU6cIqZhimy%jnZl_U?QH04WJl; z71{;J@bmx_VxVl^%dd;e%Gz_`?i(JH>6~4*rHo~cfU zapMm)wQ4U|f)Cc3G5OH`-@LM>2PuVORb&3DS}PaTwS~lG)jH&d5%u?k8ABMFNd^;%*kIN8-eLk7A){;Xpw3%LMNF7% z;b(;^YGcAN3~)Lc(f3+wRz^^IDfcd%NW6$-@!ccK4ea@$em;ao^rQj zx|U@?$uTD*()mptv$O=9n&K)&f!(VSpUfy5%83y?dEov*Z$vmjnfqn8;cGQ%`Qzpt zv)2i72{S88Go-fANqfjJ2W`;PCU7uUBPzG~lRM~$^%|{w;k}%CZPP30d^rvMG0IyC zo4Y{F%?{c_O$ed%6PXl`cVPX8ggX?6l*iFngBYmUKqrnqZG7*OWv=pI+@}dS^t_dW z;XOpNQy&T)x}E1V3UF%iJsi4g=vloBiLSIKB3I>uPy~u&W74u5n)c@L6Tm~gD+f>^ zdgdwoQ(Z%SS=ur{TOBzXWE-~-161U4GF!QLOj2R_oXEzD4^*tFPf&k+F;T9Q)OqtI zDel98=In6Komdp#>Cd52F#!%iM9l4T<)W zZN1t$t$`>|W}vh2!oe0Jv#hCZ?P_wT)2Lo>RT7A_g^6e?G?ql{0$@_w9y1{}kJtCT zxLy$*bs-Iel@oy-ewukFBgt!jW0~gI1lbp45Bc^c1}x?Vkk(xm78cGrw~0m6I+yqZ zOJeIxYh2wkGi`#by>KHyRD8|z_L0kBu>Mu3aqsjm7c_wgWBoYZUN+>H>)VGQi`TDT zvIXSfb`G1N?S|NK7W|f>?Ov8rwkD1%*M3uKGjDk0auXjem>M@zjkz$gPklUriH&4q znR!9v@WRd4-=_=^27wtQm^UwB&?WbR+wPU)WB> zB3=@#?5dTQ-Y=1HNefndBJyFpJ@Rf5^j(LE>rrKA+-UWmUN>v52QaXCaVPKqMl1Iep04>VP3Us?p*m!@&lo_cWTVx;D79gGzCs&%8_w?+lKQvDb;Tgx>; zeHBBijfm>DgPD9SHse>gPXqr}nSu%N!fglTrCSI0>t@e9R-G#NZPCS{e_L~rgvg>{ zQ)a+q#X?}Ifd%AF)NpgEYsl?06WHS2DKUmponD4^DO(eB02&sEm)3bOfKjh4PzCkA zdGm&jcrO+lQ5*6xaBCH!Ax^Tnb>_tIYrKbn#5>m%UqSq4fL-Txu-P-w-W1N;XNjff z4(BKNX;800mJA_0FV^Hr*8%KAFNn$d6egTqnT>N$+P7)59Z{CAK)`z zo@XxEO@_$>rR1S%8>0oE4oA7T(c8F&pi^;S0;mQA(>V%JwH{?UK6|s5nJ{9};Dp6T zUC@2R=)E&X5m8DhX)Tst44dRm+8=qRI}jCgxAQvWdu|BZ4bpbo=68fvFz&ZIN$F%4 z{#=C3lLwm#pW7qAvV-3;-rnk6#kOf5O*B+_c=6bGY#3a125@^Jox(v+mZEUV*I31z zG(!L}WahagEJf`MDHHsT?pq3gL~LZ@^J!M!B>?d>=QJ0_v2x@8LW9L)`B)7c1l?I! zjrz}OAm69|e}n^ra+NQR<){D}m1YJwRu~y5V*Dk`Oo4(u2tZJ|DCZZJ9%2*wT{tfd zm^GxMwC{jpGi5e;1sqMfP-gdrN;6-s0>=?}@D=<2lKXiEvI_>U{owiFz{yOCvFk|I z=|GQY@1)!8gnxFY?;d0&vF1Nw#hTv>{bRyjweRjv<;b5?Z-7~un46pPo^WnV#W2g0 zUnl>Q@?@OTvWHjU#49&&!NS5qBEUe=KXpc?J6i?1NlniR^{0IMW;i6-O!Qk))GAK>qGN zOtI_BskS-zCvOya2Vyz^R$P-#J9Rg8?=k10fZhlJ`#T9)o2P(n+j;8X08NnMv2WoK zx=2MVU|HtsFPlaHtE}>dP2lvQkhDtdwhSeJY|lLU`x(9Sb!keG)T>DQ1jWrMgJI?% zAXlfh|MpC#K_`SdMc_`M%CV%uf%Rer1zKpWP4bI^_!#jLCNn48usY$F+?&ZN>T-fC zv37*7M?f6`9%q&0HEzXVoqzY5@x%?Ig%gtYZZ?CFHYDqKtcpbQ(s5I$u+T{n{8PFO zMIk-;xs6N}-IZ2wO_|6hg0XE}_f4036XO89aq5RA4j*snLAec&Rxd_4kiV#uY}C)b zOFhW|V1BG2!T+Rg{a0w6zh$llREmGG`k8|Q%yiq&WRTvKm;ZWJ>}T`&_ghQ}Q5&HC z#6dBy5u=*y<7IMZGsPE zC`?NrI6pheTin;*?+WB{MWhFG6Qrvv{)~(B@U~hpL*YrMz@af+oILBM64s1)w?UnG zw_z(@?vg;xuFHsP$}wEQhzGNTK}yIPyIy5d0pkPM?sryJRsHtWt(8lKiHmdKluTV}4>#E4Bc^dp4yHq5x z9J(2AFUJSRrB`Yt9C|N3+*Px!zQI^H>!$Cp?h@tqR(7;S*o9^= zO*!SI8w!<=IiJ#r^Zap{LJdUHyAs!Wl$U36hx6_Q!%tD0k?V9cFbjN5>X)9thsLjv zF~$dqm;ekwD1KXjQ^2%U3-+vKTs+F6uutnS=M#Q0_(l1-Wmu-vc&mH(=7QZ+fruGg z@!!8S^Y?c4&)WhJ$APkzzTIC~7T_j^YMpZY?tiQ=0XKsF^@X;-FNS>+?BRs`T-E|g z;~_tn=Q568`)d*J@5laE)Di(~@$KiT;D-2q0wRAezO;G{Wm zWgI^jERb_3ne>Xwa&639h4b8qY~ZF-%}_MULSfdzOpSjr;Q5a= z5iI)6s-EdfXMxmY-l+VNSAKckyYt^&N4un`-NLpU0HG2h6&Or=He^a+QgGS$D={z!XZ!{OOx^E6To51Ln_grZi zlg9L2UR_9sST<|r;B$nuYPl==T8DRk4uUVY0?qsw-MM3n&&U?b<(4j;Ud|(a!hRLZ zr}2%h*-GQ8MV{(FpKPThVs3sy2yoGgN$xL^Tm$pz{XuIpsT9!#k@dyeW?@I8zH79> z6e2Ffdm|0!1W9v2(Oi92MJn=9PWb=24sb}-&)CGN&PseC^#rx4QbV-~J<9Q^` z^tLmiD9O-aZ;TTX;^ZDCS+u1n$xM_d4f3dW<9P{uVS7cUsj7)Z=vp^ z6HD(6U+%BYRp~=~F;gtc9sf{EXm0N$j1RU}>Lun`Ix!mjKF&YWHEp^&DQ8N4mim5r zEgXtNN(DJ^{ndF6|CchAL8y}Qyb5NCS=xo*R2#vW1U=e^CX@Qry{ zEP{-sY`qx(jK*lt zAktrswWoK?&#meSwh~?mPvZ`r9+$Wsu%ttZcWppFUO7{@y7H`H)udJhqkOZxzWR;D za%-urcFJ*uW|HbqU=;bSJvzdY(mRtk4$U-=y#RlWF%32G8M`h_!CN8(h^jiP)7LX_ zz1g{#UVo+TRv$=&BO-!6Rwlwt|6l~HSz=6(ej?VnUvN_H3X1@9+HxzZUax>JbG_wV^a4hp)!KGi+0&Byk^0A#b65_==4AbYnbZx%+y?=kA&;#JqX7 z0%p9lV_v8`1zARJCunsCyq^E)HE4e_DBH$T2IDfKE7{E7ZDl0TCO!)F1|)8}K90Ar zU+(mJOIIsU@Ab}kg+UdTI9ac|Vc}4)GfA3i5l2{3#q16g?SZbyx^VaQ^f=?i>3?SQ z0vKa58P12cdEk{~B!d{coRaUvh@yu%h3b~v!qA-UWBAGZ<`Wi}L*#H3_@`L3;koW4 z;t5LZf`dwqbkVSdK^MVUw}}$Ykmz3F5hZn0BZ?rS0xoslo|4I{#u>O({u9kzcua#* zO(AYo1Jy8p7c)C)){m&^d-16zNkYR@bFz#(G%_`BJx{te0LOnDKRX3_v)gTkmNFQj z?%I);)zgGHqv}aQ!qlo$hq;9yZ3?a(FQ7_ZYLv?{(G;{)gJ3D`GD9qAC0OJzbeE;; zZQ!{`oU-4sB)L}1F}%YDmiMw13zO^F2QSf}Lo;fGn zHW`rEh?c60?*qgJ>Qj#?p@y8|csupFTpt%fV-p3V@c67IrMeVOFDS z$;*K1^cO}l{2~|PEXi);8~e>4S=t)I2+f+&Kiy0cP+l-B$u7i9iF8n@)R<%1Dk2P# zPDI4;_y-5$o?mc|MqMdp3|Z~E=g6st^LC;p9hgS%r1ehQrnRuV=hE)*m>0{uzY68G zcL3V+ZZgS?a!4}-Ro>j*p)o1Pn5ZM;$Q?nb%G&ZiGk?_fETt;?TBymLs9M_M`?|dR z9%TGfsw4kC?kzjefiZZe3Q1RVmU*nD?Sxtk)tfk81OeR@#N6i)Yo-_nE-YrxG$4AH zJ&EyosleApB|E=y9W~U@@oZj-(Vf2X8}QWz1e!z$Flx&KM(!d!o6fWBow+xy<7f2- zVu*IW6pyiub~Hr-o(5Brj7B6I>6Z>i+xp;VR*-(DD!TaL%Cn-jwqg} zTsRQ~Mk9m`=0{a+`Ozf#7Sf<;PLD`_({uw0rG`7NqKk+0|1PX{TfnQ?`&OBlG;sF6 zOLx^2BlYx;!>@#zWEhy%DcwqZ9u6qv`FdmH-O(Wd$+Y zpY2Eb9ONG1tWzY}>H!Sh-RhDwhB-!gd_*UX`8=ZM~SOCS=|h=K3AyR z+Tibe95;J+i?H^!Js!+<2Klyi9{X6@n97LP>~M!Rkd6}T zoQ70NQoX*?n&qcgvb1Od`AvB@gO)K=*-b9_(pN^YPtBX7?Y{KT8`rcU}{O6Obu zj@(2IJI&^|uefIj4@y5iVNfdy)n&h=9;U>4HKdjl7UtZ-Y1Dh)@+-=u1oZ~Q2lcB~ zJnu1?wht(q`DSPZA>zD->Q_oqmk>g=`GL8qwnHk9V|3vH`FpNtFGLSVSk^AtV)DnK znqz>Brn^g;RB3BbUMY!1iUkn;7s%Y9z;>6>s|ZG(0lFEQfiY4a5`oCBA_d^{Vk$GC zDrEN;(YnA*^{MhB;)cSKGdhHoXW_Hu?nI9nT9Z5x;cgI56Rh8CUWiOIYpI7b#1fYx3GnA4|~g-D8<& zS(5z>(`9d(2?Phlq6%An4AoP5xL7kE>0Xgnt~Y6wzY^aBHR>oV%r#H-Sbr2K)r_Mf z`7N7Up}6RjE8332)VXMZ;d6ebR;C8X;nf6&5!pRr`nndQ^=%fp-P?98FT8VO3ftb& zxrSx)RPBLp>z@sb`upD0=oe1bbo-R!2h<-h8hU&J45u`OB0c7#3nv#1@9KA+){fVT z#W~T-`Urzswv(4Mwc|15u(9#74#FUYI-yH3wiE1+weMH_vKZ$lH(Fd77na***D z2BM+MIF{fclBhJS=;Shph?owbGn$(n{iF(o2Q@Ne5DgEYx)8)@p=Ywiq$bGFX6~E7 zefF;0K`IZl}ul3570d&6i)h(OA5zLiz6&CD&2}5RqWu7=o04=M%0w3w_*RY$o8>k-xzpO~P+F!`-Vh7aUn+#sV*g(w)_|cN#tH~cl zgnZyapS{F&Q5MNobpp8hmf7y(!tT-qyvTog@)0Y)1!}RN9;4B+ANRP%ffrpD_iz3Z z1gKtJ0utF1bC&Evgta&DV*K|Vzp{Csbl}$_QOntEEc?G(9N2Yk85*y?+za(Tn7??v Nc;4V#_F3yY{|0X;yWju- literal 0 HcmV?d00001 diff --git a/overwatch/processing/alarms/doc/last2.png b/overwatch/processing/alarms/doc/last2.png new file mode 100644 index 0000000000000000000000000000000000000000..3f8a1f4e51d97ff35393dfe4ec330ccc87a97728 GIT binary patch literal 15607 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 @$^w|A5jnd{nkoiNFp20p~R=bZjgj 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 Date: Fri, 14 Dec 2018 01:09:08 +0100 Subject: [PATCH 35/38] Improved displaying alarms on webApp --- overwatch/processing/trending/manager.py | 5 +- .../webApp/templates/runPageMainContent.html | 48 +++++++++++++------ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/overwatch/processing/trending/manager.py b/overwatch/processing/trending/manager.py index f8ace115..d3d6db45 100644 --- a/overwatch/processing/trending/manager.py +++ b/overwatch/processing/trending/manager.py @@ -168,6 +168,7 @@ def notifyAboutNewHistogramValue(self, hist): # type: (histogramContainer) -> N for alarm in trend.alarms: alarm.processCheck(trend) if trend.alarmsMessages: - hist.information[trend.name] = '\n'.join(trend.alarmsMessages) + hist.information["Alarm" + trend.name] = '\n'.join(trend.alarmsMessages) trend.alarmsMessages = [] - alarmCollector.showOnConsole() + alarmCollector.showOnConsole() + # alarmCollector.announceOnSlack() diff --git a/overwatch/webApp/templates/runPageMainContent.html b/overwatch/webApp/templates/runPageMainContent.html index 37c19c63..d102f84a 100644 --- a/overwatch/webApp/templates/runPageMainContent.html +++ b/overwatch/webApp/templates/runPageMainContent.html @@ -25,6 +25,7 @@

{{ run.prettyName }}

{%- if selectedHistGroup == histGroup.selectionPattern or (selectedHistGroup == None and firstLoopCompleted == []) -%} {%- for histName in histGroup.histList -%} {%- set hist = subsystem.hists[histName] -%} + {%- set alarms = {} -%} {%- if selectedHist == hist.histName or selectedHist == None -%} {%- if histGroup.plotInGrid == False or (histGroup.plotInGrid == True and firstLoopCompleted == []) -%} {# Effective increments our counter #} @@ -41,24 +42,41 @@

{{ hist.prettyName }}

{%- if label == "Threshold" -%} {%- if threshold.append(info) -%}{%- endif -%} {%- endif -%} - -
{{ label }}
+ {%- if label.startswith("Alarm") -%} + {%- set trend = label.split("Alarm")[1] -%} + {%- if alarms.update({trend: info}) -%}{%- endif -%} + {%- else -%} + +
{{ label }}
+ +
+ +
{{ info }}
+
+
+ {%- endif -%} + {% endfor -%} + {%- if alarms -%} + +
Alarms
-
- - {%- if label not in ("Threshold", "Fast OR Hot Channels ID")-%} -
+ + + {%- for trend, info in alarms.items() -%} {% for infoData in info.split('\n') %} -
{{ infoData }}
- {% endfor %} -
- {% else %} + + +
{{ infoData }}
+
+
+ {% endfor -%} + {% endfor -%}
{{ info }}
- {% endif -%} -
-
- {% endfor -%} +
+
+ {% endif -%} {% endif -%} {% endif -%} {# If grid, then add class #} From ddc895ee04e475a6712a1633d500d189383f1f58 Mon Sep 17 00:00:00 2001 From: arturro96 Date: Fri, 14 Dec 2018 22:26:19 +0100 Subject: [PATCH 36/38] Displaying fix --- overwatch/processing/detectors/EMC.py | 68 ++++++------- overwatch/processing/detectors/HLT.py | 96 +++++++++++-------- overwatch/processing/detectors/TPC.py | 10 +- .../webApp/templates/runPageMainContent.html | 4 +- 4 files changed, 101 insertions(+), 77 deletions(-) diff --git a/overwatch/processing/detectors/EMC.py b/overwatch/processing/detectors/EMC.py index 67a10243..13dd4b7f 100644 --- a/overwatch/processing/detectors/EMC.py +++ b/overwatch/processing/detectors/EMC.py @@ -54,36 +54,36 @@ def getTrendingObjectInfo(): # To quick add data we iterate over info list and example trendingObjects # info list has format: ["depending histogram name and also trending name", "desc"] infoList = [ - ("EMCTRQA_histAmpEdgePosEMCGAHOffline", "Integrated amplitude EMCGAH patch Offline"), - ("EMCTRQA_histAmpEdgePosEMCGAHOnline", "Integrated amplitude EMCGAH patch Online"), - ("EMCTRQA_histAmpEdgePosEMCGAHRecalc", "Integrated amplitude EMCGAH patch Recalc"), - ("EMCTRQA_histAmpEdgePosEMCGALOnline", "Integrated amplitude EMCGAL patch Online"), - ("EMCTRQA_histAmpEdgePosEMCJEHOffline", "Integrated amplitude EMCJEH patch Offline"), - ("EMCTRQA_histAmpEdgePosEMCJEHOnline", "Integrated amplitude EMCJEH patch Online"), - ("EMCTRQA_histAmpEdgePosEMCJEHRecalc", "Integrated amplitude EMCJEH patch Recalc"), - ("EMCTRQA_histAmpEdgePosEMCJELOnline", "Integrated amplitude EMCJEL patch Online"), - ("EMCTRQA_histAmpEdgePosEMCL0Offline", "Integrated amplitude EMCL0 patch Offline"), - ("EMCTRQA_histAmpEdgePosEMCL0Online", "Integrated amplitude EMCL0 patch Online"), - ("EMCTRQA_histAmpEdgePosEMCL0Recalc", "Integrated amplitude EMCL0 patch Recalc"), - ("EMCTRQA_histEvents", "Number of events"), - ("EMCTRQA_histMaxEdgePosEMCGAHOffline", "Edge Position Max EMCGAH patch Offline"), - ("EMCTRQA_histMaxEdgePosEMCGAHOnline", "Edge Position Max EMCGAH patch Online"), - ("EMCTRQA_histMaxEdgePosEMCGAHRecalc", "Edge Position Max EMCGAH patch Recalc"), - ("EMCTRQA_histMaxEdgePosEMCGALOnline", "Edge Position Max EMCGAL patch Online"), - ("EMCTRQA_histMaxEdgePosEMCJEHOffline", "Edge Position Max EMCJEH patch Offline"), - ("EMCTRQA_histMaxEdgePosEMCJEHOnline", "Edge Position Max EMCJEH patch Online"), - ("EMCTRQA_histMaxEdgePosEMCJEHRecalc", "Edge Position Max EMCJEH patch Recalc"), - ("EMCTRQA_histMaxEdgePosEMCJELOnline", "Edge Position Max EMCJEL patch Online"), - ("EMCTRQA_histMaxEdgePosEMCL0Offline", "Edge Position Max EMCL0 patch Offline"), - ("EMCTRQA_histMaxEdgePosEMCL0Online", "Edge Position Max EMCL0 patch Online"), - ("EMCTRQA_histMaxEdgePosEMCL0Recalc", "Edge Position Max EMCL0 patch Recalc"), - ("EMCTRQA_histFastORL0", "L0 entries vs FastOR number"), - ("EMCTRQA_histFastORL0Amp", "L0 amplitudes vs position"), - ("EMCTRQA_histFastORL0LargeAmp", "L0 (amp>400) vs FastOR number"), - ("EMCTRQA_histFastORL0Time", "L0 trigger time vs FastOR number"), - ("EMCTRQA_histFastORL1", "L1 entries vs FastOR number"), - ("EMCTRQA_histFastORL1Amp", "L1 amplitudes"), - ("EMCTRQA_histFastORL1LargeAmp", "L1 (amp>400)"), + ("AmpEdgePosEMCGAHOffline", "Integrated amplitude EMCGAH patch Offline", ["EMCTRQA_histAmpEdgePosEMCGAHOffline"]), + ("AmpEdgePosEMCGAHOnline", "Integrated amplitude EMCGAH patch Online", ["EMCTRQA_histAmpEdgePosEMCGAHOnline"]), + ("AmpEdgePosEMCGAHRecalc", "Integrated amplitude EMCGAH patch Recalc", ["EMCTRQA_histAmpEdgePosEMCGAHRecalc"]), + ("AmpEdgePosEMCGALOnline", "Integrated amplitude EMCGAL patch Online", ["EMCTRQA_histAmpEdgePosEMCGALOnline"]), + ("AmpEdgePosEMCJEHOffline", "Integrated amplitude EMCJEH patch Offline", ["EMCTRQA_histAmpEdgePosEMCJEHOffline"]), + ("AmpEdgePosEMCJEHOnline", "Integrated amplitude EMCJEH patch Online", ["EMCTRQA_histAmpEdgePosEMCJEHOnline"]), + ("AmpEdgePosEMCJEHRecalc", "Integrated amplitude EMCJEH patch Recalc", ["EMCTRQA_histAmpEdgePosEMCJEHRecalc"]), + ("AmpEdgePosEMCJELOnline", "Integrated amplitude EMCJEL patch Online", ["EMCTRQA_histAmpEdgePosEMCJELOnline"]), + ("AmpEdgePosEMCL0Offline", "Integrated amplitude EMCL0 patch Offline", ["EMCTRQA_histAmpEdgePosEMCL0Offline"]), + ("AmpEdgePosEMCL0Online", "Integrated amplitude EMCL0 patch Online", ["EMCTRQA_histAmpEdgePosEMCL0Online"]), + ("AmpEdgePosEMCL0Recalc", "Integrated amplitude EMCL0 patch Recalc", ["EMCTRQA_histAmpEdgePosEMCL0Recalc"]), + ("Events", "Number of events", ["EMCTRQA_histEvents"]), + ("MaxEdgePosEMCGAHOffline", "Edge Position Max EMCGAH patch Offline", ["EMCTRQA_histMaxEdgePosEMCGAHOffline"]), + ("MaxEdgePosEMCGAHOnline", "Edge Position Max EMCGAH patch Online", ["EMCTRQA_histMaxEdgePosEMCGAHOnline"]), + ("MaxEdgePosEMCGAHRecalc", "Edge Position Max EMCGAH patch Recalc", ["EMCTRQA_histMaxEdgePosEMCGAHRecalc"]), + ("MaxEdgePosEMCGALOnline", "Edge Position Max EMCGAL patch Online", ["EMCTRQA_histMaxEdgePosEMCGALOnline"]), + ("MaxEdgePosEMCJEHOffline", "Edge Position Max EMCJEH patch Offline", ["EMCTRQA_histMaxEdgePosEMCJEHOffline"]), + ("MaxEdgePosEMCJEHOnline", "Edge Position Max EMCJEH patch Online", ["EMCTRQA_histMaxEdgePosEMCJEHOnline"]), + ("MaxEdgePosEMCJEHRecalc", "Edge Position Max EMCJEH patch Recalc", ["EMCTRQA_histMaxEdgePosEMCJEHRecalc"]), + ("MaxEdgePosEMCJELOnline", "Edge Position Max EMCJEL patch Online", ["EMCTRQA_histMaxEdgePosEMCJELOnline"]), + ("MaxEdgePosEMCL0Offline", "Edge Position Max EMCL0 patch Offline", ["EMCTRQA_histMaxEdgePosEMCL0Offline"]), + ("MaxEdgePosEMCL0Online", "Edge Position Max EMCL0 patch Online", ["EMCTRQA_histMaxEdgePosEMCL0Online"]), + ("MaxEdgePosEMCL0Recalc", "Edge Position Max EMCL0 patch Recalc", ["EMCTRQA_histMaxEdgePosEMCL0Recalc"]), + ("FastORL0", "L0 entries vs FastOR number", ["EMCTRQA_histFastORL0"]), + ("FastORL0Amp", "L0 amplitudes vs position", ["EMCTRQA_histFastORL0Amp"]), + ("FastORL0LargeAmp", "L0 (amp>400) vs FastOR number", ["EMCTRQA_histFastORL0LargeAmp"]), + ("FastORL0Time", "L0 trigger time vs FastOR number", ["EMCTRQA_histFastORL0Time"]), + ("FastORL1", "L1 entries vs FastOR number", ["EMCTRQA_histFastORL1"]), + ("FastORL1Amp", "L1 amplitudes", ["EMCTRQA_histFastORL1Amp"]), + ("FastORL1LargeAmp", "L1 (amp>400)", ["EMCTRQA_histFastORL1LargeAmp"]), ] trendingNameToObject = { "max": trendingObjects.MaximumTrending, @@ -97,11 +97,11 @@ def getTrendingObjectInfo(): } trendingInfo = [] for prefix, cls in trendingNameToObject.items(): - for dependingFile, desc in infoList: - infoObject = TrendingInfo(prefix + dependingFile, desc, [dependingFile], cls) + for name, desc, histograms in infoList: + ti = TrendingInfo(prefix + name, prefix + ": " + desc, histograms, cls) if prefix in alarms: - infoObject.addAlarm(alarms[prefix]) - trendingInfo.append(infoObject) + ti.addAlarm(alarms[prefix]) + trendingInfo.append(ti) return trendingInfo def checkForEMCHistStack(subsystem, histName, skipList, selector): diff --git a/overwatch/processing/detectors/HLT.py b/overwatch/processing/detectors/HLT.py index a39825cc..cca88074 100644 --- a/overwatch/processing/detectors/HLT.py +++ b/overwatch/processing/detectors/HLT.py @@ -15,6 +15,7 @@ import ROOT from overwatch.processing.trending.info import TrendingInfo import overwatch.processing.trending.objects as trendingObjects +from overwatch.processing.alarms.example import alarmStdConfig, alarmMaxConfig, alarmMeanConfig def generalHLTOptions(subsystem, hist, processingOptions, **kwargs): """ Specify general HLT histogram options. @@ -67,52 +68,67 @@ def getTrendingObjectInfo(): # To quick add data we iterate over info list and example trendingObjects # info list has format: ["depending histogram name and also trending name", "desc"] infoList = [ - ("fHistClusterChargeMax", "TPC Cluster ChargeMax"), - ("fHistClusterChargeTot", "TPC Cluster ChargeTotal"), - ("fHistHLTInSize_HLTOutSize", "HLT Out Size vs HLT In Size"), - ("fHistHLTSize_HLTInOutRatio", "HLT Out/In Size Ratio vs HLT Input Size"), - ("fHistSDDclusters_SDDrawSize", "SDD clusters vs SDD raw size"), - ("fHistSPDclusters_SDDclusters", "SDD clusters vs SPD clusters"), - ("fHistSPDclusters_SPDrawSize", "SPD clusters vs SPD raw size"), - ("fHistSPDclusters_SSDclusters", "SSD clusters vs SPD clusters"), - ("fHistSSDclusters_SDDclusters", "SDD clusters vs SSD clusters"), - ("fHistSSDclusters_SSDrawSize", "SSD clusters vs SSD raw size"), - ("fHistTPCAallClustersRowPhi", "TPCA clusters all, raw cluster coordinates"), - ("fHistTPCAattachedClustersRowPhi", "TPCA clusters attached to tracks, raw cluster coordinates"), - ("fHistTPCCallClustersRowPhi", "TPCC clusters all, raw cluster coordinates"), - ("fHistTPCCattachedClustersRowPhi", "TPCC clusters attached to tracks, raw cluster coordinates"), - ("fHistTPCClusterFlags", "TPC Cluster Flags"), - ("fHistTPCClusterSize_TPCCompressedSize", "TPC compressed size vs TPC HWCF Size"), - ("fHistTPCHLTclusters_TPCCompressionRatio", "Huffman compression ratio vs TPC HLT clusters"), - ("fHistTPCHLTclusters_TPCFullCompressionRatio", "Full compression ratio vs TPC HLT clusters"), - ("fHistTPCHLTclusters_TPCSplitClusterRatioPad", "TPC Split Cluster ratio pad vs TPC HLT clusters"), - ("fHistTPCHLTclusters_TPCSplitClusterRatioTime", "TPC Split Cluster ratio time vs TPC HLT clusters"), - ("fHistTPCRawSize_TPCCompressedSize", "TPC compressed size vs TPC Raw Size"), - ("fHistTPCTrackPt", "TPC Track Pt"), - ("fHistTPCdEdxMaxIROC", "TPC dE/dx v.s. P (qMax, IROC)"), - ("fHistTPCdEdxMaxOROC1", "TPC dE/dx v.s. P (qMax, OROC1)"), - ("fHistTPCdEdxMaxOROC2", "TPC dE/dx v.s. P (qMax, OROC2)"), - ("fHistTPCdEdxMaxOROCAll", "TPC dE/dx v.s. P (qMax, OROC all)"), - ("fHistTPCdEdxMaxTPCAll", "TPC dE/dx v.s. P (qMax, full TPC)"), - ("fHistTPCdEdxTotIROC", "TPC dE/dx v.s. P (qTot, IROC)"), - ("fHistTPCdEdxTotOROC1", "TPC dE/dx v.s. P (qTot, OROC1)"), - ("fHistTPCdEdxTotOROC2", "TPC dE/dx v.s. P (qTot, OROC2)"), - ("fHistTPCdEdxTotOROCAll", "TPC dE/dx v.s. P (qTot, OROC all)"), - ("fHistTPCdEdxTotTPCAll", "TPC dE/dx v.s. P (qTot, full TPC)"), - ("fHistTPCtracks_TPCtracklets", "TPC Tracks vs TPC Tracklets"), - ("fHistTZERO_ITSSPDVertexZ", "TZERO interaction time vs ITS vertex z"), - ("fHistVZERO_SPDClusters", "SPD Clusters vs VZERO Trigger Charge (A+C)"), - ("fHistZNA_VZEROTrigChargeA", "ZNA vs. VZERO Trigger Charge A"), - ("fHistZNC_VZEROTrigChargeC", "ZNC vs. VZERO Trigger Charge C"), - ("fHistZNT_VZEROTrigChargeT", "ZN (A+C) vs. VZERO Trigger Charge (A+C)"), + ("ClusterChargeMax", "TPC Cluster ChargeMax", ["fHistClusterChargeMax"]), + ("ClusterChargeTot", "TPC Cluster ChargeTotal", ["fHistClusterChargeTot"]), + ("HLTInSize_HLTOutSize", "HLT Out Size vs HLT In Size", ["fHistHLTInSize_HLTOutSize"]), + ("HLTSize_HLTInOutRatio", "HLT Out/In Size Ratio vs HLT Input Size", ["fHistHLTSize_HLTInOutRatio"]), + ("SDDclusters_SDDrawSize", "SDD clusters vs SDD raw size", ["fHistSDDclusters_SDDrawSize"]), + ("SPDclusters_SDDclusters", "SDD clusters vs SPD clusters", ["fHistSPDclusters_SDDclusters"]), + ("SPDclusters_SPDrawSize", "SPD clusters vs SPD raw size", ["fHistSPDclusters_SPDrawSize"]), + ("SPDclusters_SSDclusters", "SSD clusters vs SPD clusters", ["fHistSPDclusters_SSDclusters"]), + ("SSDclusters_SDDclusters", "SDD clusters vs SSD clusters", ["fHistSSDclusters_SDDclusters"]), + ("SSDclusters_SSDrawSize", "SSD clusters vs SSD raw size", ["fHistSSDclusters_SSDrawSize"]), + ("TPCAallClustersRowPhi", "TPCA clusters all, raw cluster coordinates", ["fHistTPCAallClustersRowPhi"]), + ("TPCAattachedClustersRowPhi", "TPCA clusters attached to tracks, raw cluster coordinates", + ["fHistTPCAattachedClustersRowPhi"]), + ("TPCCallClustersRowPhi", "TPCC clusters all, raw cluster coordinates", ["fHistTPCCallClustersRowPhi"]), + ("TPCCattachedClustersRowPhi", "TPCC clusters attached to tracks, raw cluster coordinates", + ["fHistTPCCattachedClustersRowPhi"]), + ("TPCClusterFlags", "TPC Cluster Flags", ["fHistTPCClusterFlags"]), + ("TPCClusterSize_TPCCompressedSize", "TPC compressed size vs TPC HWCF Size", + ["fHistTPCClusterSize_TPCCompressedSize"]), + ("TPCHLTclusters_TPCCompressionRatio", "Huffman compression ratio vs TPC HLT clusters", + ["fHistTPCHLTclusters_TPCCompressionRatio"]), + ("TPCHLTclusters_TPCFullCompressionRatio", "Full compression ratio vs TPC HLT clusters", + ["fHistTPCHLTclusters_TPCFullCompressionRatio"]), + ("TPCHLTclusters_TPCSplitClusterRatioPad", "TPC Split Cluster ratio pad vs TPC HLT clusters", + ["fHistTPCHLTclusters_TPCSplitClusterRatioPad"]), + ("TPCHLTclusters_TPCSplitClusterRatioTime", "TPC Split Cluster ratio time vs TPC HLT clusters", + ["fHistTPCHLTclusters_TPCSplitClusterRatioTime"]), + ("TPCRawSize_TPCCompressedSize", "TPC compressed size vs TPC Raw Size", ["fHistTPCRawSize_TPCCompressedSize"]), + ("TPCTrackPt", "TPC Track Pt", ["fHistTPCTrackPt"]), + ("TPCdEdxMaxIROC", "TPC dE/dx v.s. P (qMax, IROC)", ["fHistTPCdEdxMaxIROC"]), + ("TPCdEdxMaxOROC1", "TPC dE/dx v.s. P (qMax, OROC1)", ["fHistTPCdEdxMaxOROC1"]), + ("TPCdEdxMaxOROC2", "TPC dE/dx v.s. P (qMax, OROC2)", ["fHistTPCdEdxMaxOROC2"]), + ("TPCdEdxMaxOROCAll", "TPC dE/dx v.s. P (qMax, OROC all)", ["fHistTPCdEdxMaxOROCAll"]), + ("TPCdEdxMaxTPCAll", "TPC dE/dx v.s. P (qMax, full TPC)", ["fHistTPCdEdxMaxTPCAll"]), + ("TPCdEdxTotIROC", "TPC dE/dx v.s. P (qTot, IROC)", ["fHistTPCdEdxTotIROC"]), + ("TPCdEdxTotOROC1", "TPC dE/dx v.s. P (qTot, OROC1)", ["fHistTPCdEdxTotOROC1"]), + ("TPCdEdxTotOROC2", "TPC dE/dx v.s. P (qTot, OROC2)", ["fHistTPCdEdxTotOROC2"]), + ("TPCdEdxTotOROCAll", "TPC dE/dx v.s. P (qTot, OROC all)", ["fHistTPCdEdxTotOROCAll"]), + ("TPCdEdxTotTPCAll", "TPC dE/dx v.s. P (qTot, full TPC)", ["fHistTPCdEdxTotTPCAll"]), + ("TPCtracks_TPCtracklets", "TPC Tracks vs TPC Tracklets", ["fHistTPCtracks_TPCtracklets"]), + ("TZERO_ITSSPDVertexZ", "TZERO interaction time vs ITS vertex z", ["fHistTZERO_ITSSPDVertexZ"]), + ("VZERO_SPDClusters", "SPD Clusters vs VZERO Trigger Charge (A+C)", ["fHistVZERO_SPDClusters"]), + ("ZNA_VZEROTrigChargeA", "ZNA vs. VZERO Trigger Charge A", ["fHistZNA_VZEROTrigChargeA"]), + ("ZNC_VZEROTrigChargeC", "ZNC vs. VZERO Trigger Charge C", ["fHistZNC_VZEROTrigChargeC"]), + ("ZNT_VZEROTrigChargeT", "ZN (A+C) vs. VZERO Trigger Charge (A+C)", ["fHistZNT_VZEROTrigChargeT"]), ] trendingNameToObject = { "max": trendingObjects.MaximumTrending, "mean": trendingObjects.MeanTrending, "stdDev": trendingObjects.StdDevTrending, } + alarms = { + "max": alarmMaxConfig(), + "mean": alarmMeanConfig(), + "stdDev": alarmStdConfig() + } trendingInfo = [] for prefix, cls in trendingNameToObject.items(): - for dependingFile, desc in infoList: - trendingInfo.append(TrendingInfo(prefix + dependingFile, desc, [dependingFile], cls)) + for name, desc, histograms in infoList: + ti = TrendingInfo(prefix + name, prefix + ": " + desc, histograms, cls) + if prefix in alarms: + ti.addAlarm(alarms[prefix]) + trendingInfo.append(ti) return trendingInfo diff --git a/overwatch/processing/detectors/TPC.py b/overwatch/processing/detectors/TPC.py index 8e6e791a..f8f79d8c 100644 --- a/overwatch/processing/detectors/TPC.py +++ b/overwatch/processing/detectors/TPC.py @@ -23,6 +23,7 @@ from overwatch.processing.trending.info import TrendingInfo import overwatch.processing.trending.objects as trendingObjects +from overwatch.processing.alarms.example import alarmStdConfig, alarmMaxConfig, alarmMeanConfig try: from typing import * # noqa @@ -63,10 +64,17 @@ def getTrendingObjectInfo(): # type: () -> List[TrendingInfo] "mean": trendingObjects.MeanTrending, "stdDev": trendingObjects.StdDevTrending, } + alarms = { + "max": alarmMaxConfig(), + "mean": alarmMeanConfig(), + "stdDev": alarmStdConfig() + } trendingInfo = [] for prefix, cls in trendingNameToObject.items(): for name, desc, histograms in infoList: - ti = TrendingInfo(prefix + name, prefix + desc, histograms, cls) + ti = TrendingInfo(prefix + name, prefix + ": " + desc, histograms, cls) + if prefix in alarms: + ti.addAlarm(alarms[prefix]) trendingInfo.append(ti) return trendingInfo diff --git a/overwatch/webApp/templates/runPageMainContent.html b/overwatch/webApp/templates/runPageMainContent.html index d102f84a..040dd5e7 100644 --- a/overwatch/webApp/templates/runPageMainContent.html +++ b/overwatch/webApp/templates/runPageMainContent.html @@ -58,12 +58,12 @@

{{ hist.prettyName }}

{%- endif -%} {% endfor -%} {%- if alarms -%} - +
Alarms
+ id="collapseAlarm{{ hist.histName.replace("/", "") }}"> {%- for trend, info in alarms.items() -%} {% for infoData in info.split('\n') %} From 1e27f9ec23d504fd92e7641a7f93a9f5dfb8b8bf Mon Sep 17 00:00:00 2001 From: ostro Date: Sat, 29 Dec 2018 14:15:30 +0100 Subject: [PATCH 37/38] Alarm can invoke itself or by collector, fix tests --- overwatch/processing/alarms/alarm.py | 18 +++++++++++++----- overwatch/processing/alarms/collectors.py | 8 ++++---- .../alarms/impl/betweenValuesAlarm.py | 4 ++-- tests/processing/alarms/test_alarms.py | 2 ++ tests/unit/fixtures/alarmFixtures.py | 1 + 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/overwatch/processing/alarms/alarm.py b/overwatch/processing/alarms/alarm.py index 8e31125a..8b574d32 100644 --- a/overwatch/processing/alarms/alarm.py +++ b/overwatch/processing/alarms/alarm.py @@ -15,8 +15,9 @@ class Alarm(object): - def __init__(self, alarmText=''): + def __init__(self, alarmText='', collector=None): self.alarmText = alarmText + self.collector = collector self.receivers = [] self.parent = None # type: Optional[AggregatingAlarm] @@ -27,11 +28,19 @@ def processCheck(self, trend=None): # type: (Optional[TrendingObject]) -> None args = (self.prepareTrendValues(trend),) if trend else () result = self.checkAlarm(*args) isAlarm, msg = result - msg = "[{alarmText}]: {msg}".format(alarmText=self.alarmText, msg=msg) if isAlarm: - trend.alarmsMessages.append(msg) - alarmCollector.collectMessage(self, "[{}]".format(trend.name) + msg) + msg = "[{alarmText}]: {msg}".format(alarmText=self.alarmText, msg=msg) + + # aggregating alarms don't have trend + if trend: + trend.alarmsMessages.append(msg) + + # tell collector about alarm or announce alarm itself + if self.collector: + alarmCollector.collectMessage(self, "[{trendName}]{msg}".format(trendName=trend.name, msg=msg)) + else: + self._announceAlarm(msg) if self.parent: self.parent.childProcessed(child=self, result=isAlarm) @@ -49,6 +58,5 @@ def checkAlarm(self, trend): # type: (np.ndarray) -> (bool, str) raise NotImplementedError def _announceAlarm(self, msg): # type: (str) -> None - msg = "[{alarmText}]: {msg}".format(alarmText=self.alarmText, msg=msg) for receiver in self.receivers: receiver(msg) diff --git a/overwatch/processing/alarms/collectors.py b/overwatch/processing/alarms/collectors.py index a735e7d0..542275a0 100644 --- a/overwatch/processing/alarms/collectors.py +++ b/overwatch/processing/alarms/collectors.py @@ -129,9 +129,9 @@ def sendMessage(self, payload): fail = "Slack not configured, couldn't send messages" if 'slack' in self.parameters: - self.slackClient.api_call('chat.postMessage', channel=self.channel, - text=payload, username='Alarms OVERWATCH', - icon_emoji=':robot_face:') + self.slackClient.api_call( + 'chat.postMessage', channel=self.channel, text=payload, + username='Alarms OVERWATCH', icon_emoji=':robot_face:') logger.debug(success.format(channel=self.channel)) else: logger.debug(fail) @@ -184,7 +184,7 @@ def announceOnSlack(self): Args: None. - Return: + Return:split previousValue into absolute and relative None. """ if SlackNotification() in self.receivers: diff --git a/overwatch/processing/alarms/impl/betweenValuesAlarm.py b/overwatch/processing/alarms/impl/betweenValuesAlarm.py index 59dae98b..907b1b3b 100644 --- a/overwatch/processing/alarms/impl/betweenValuesAlarm.py +++ b/overwatch/processing/alarms/impl/betweenValuesAlarm.py @@ -7,8 +7,8 @@ class BetweenValuesAlarm(Alarm): - def __init__(self, centerValue=50., maxDistance=50., minVal=None, maxVal=None, alarmText=''): - super(BetweenValuesAlarm, self).__init__(alarmText=alarmText) + def __init__(self, centerValue=50., maxDistance=50., minVal=None, maxVal=None, alarmText='', *args, **kwargs): + super(BetweenValuesAlarm, self).__init__(alarmText=alarmText, *args, **kwargs) self.minVal = minVal if minVal is not None else centerValue - maxDistance self.maxVal = maxVal if maxVal is not None else centerValue + maxDistance diff --git a/tests/processing/alarms/test_alarms.py b/tests/processing/alarms/test_alarms.py index 4587fe28..e30abb21 100644 --- a/tests/processing/alarms/test_alarms.py +++ b/tests/processing/alarms/test_alarms.py @@ -7,6 +7,8 @@ from overwatch.processing.alarms.impl.absolutePreviousValueAlarm import AbsolutePreviousValueAlarm from overwatch.processing.alarms.impl.betweenValuesAlarm import BetweenValuesAlarm +from overwatch.processing.alarms.impl.checkLastNAlarm import CheckLastNAlarm +from overwatch.processing.alarms.impl.meanInRangeAlarm import MeanInRangeAlarm from overwatch.processing.alarms.impl.relativePreviousValueAlarm import RelativePreviousValueAlarm diff --git a/tests/unit/fixtures/alarmFixtures.py b/tests/unit/fixtures/alarmFixtures.py index eaf61863..6c47dc39 100644 --- a/tests/unit/fixtures/alarmFixtures.py +++ b/tests/unit/fixtures/alarmFixtures.py @@ -19,6 +19,7 @@ def __init__(self, name=''): self.name = name self.alarms = [] self.trendedValues = [] + self.alarmsMessages = [] def addNewValue(self, val): self.trendedValues.append(val) From 77cc59f483b183996a397ffbdeb94861a1110839 Mon Sep 17 00:00:00 2001 From: ostro Date: Sat, 29 Dec 2018 14:15:48 +0100 Subject: [PATCH 38/38] update example --- overwatch/processing/alarms/example.py | 37 +++++++++++++++++--------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/overwatch/processing/alarms/example.py b/overwatch/processing/alarms/example.py index ed44802d..ef40883d 100644 --- a/overwatch/processing/alarms/example.py +++ b/overwatch/processing/alarms/example.py @@ -5,11 +5,11 @@ from overwatch.processing.alarms.impl.meanInRangeAlarm import MeanInRangeAlarm - class TrendingObjectMock: def __init__(self, alarms): self.alarms = alarms self.trendedValues = [] + self.alarmsMessages = [] def addNewValue(self, val): self.trendedValues.append(val) @@ -17,29 +17,37 @@ def addNewValue(self, val): def checkAlarms(self): for alarm in self.alarms: - alarm.checkAlarm(self) + alarm.processCheck(self) def __str__(self): return self.__class__.__name__ def alarmConfig(): - recipients = ["test1@mail", "test2@mail"] - mailSender = MailSender(recipients) - slackSender = SlackNotification() - borderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") + # recipients = ["test1@mail", "test2@mail"] + # mailSender = MailSender(recipients) + # slackSender = SlackNotification() + + # for example purpose: + def mailSender(x): + printCollector("MAIL:" + x) + + def slackSender(x): + printCollector("Slack: " + x) + borderWarning = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="WARNING") borderWarning.receivers = [printCollector] borderError = BetweenValuesAlarm(minVal=0, maxVal=70, alarmText="ERROR") borderError.receivers = [mailSender, slackSender] - bva = BetweenValuesAlarm(minVal=0, maxVal=90, alarmText='BETWEEN') - # TODO add second alarm to andAlarm - seriousAlarm = AndAlarm([bva], "Serious Alarm") + bva = BetweenValuesAlarm(minVal=0, maxVal=50, alarmText="BETWEEN") + clna = CheckLastNAlarm(minVal=0, maxVal=70, ratio=0.6, N=3, alarmText="LastN") + seriousAlarm = AndAlarm([bva, clna], "Serious Alarm") seriousAlarm.addReceiver(mailSender) - return [borderWarning, borderError, seriousAlarm] + return [borderWarning, borderError, bva, clna] + def alarmMeanConfig(): slack = SlackNotification() @@ -53,21 +61,24 @@ def alarmMeanConfig(): return [lastAlarm, borderWarning] + def alarmStdConfig(): slack = SlackNotification() - meanInRangeWarning = MeanInRangeAlarm(alarmText="WARNING") + meanInRangeWarning = MeanInRangeAlarm(alarmText="WARNING", collector=True) meanInRangeWarning.receivers = [printCollector, slack] return [meanInRangeWarning] + def alarmMaxConfig(): recipients = ["test@mail"] mailSender = MailSender(recipients) - borderError = BetweenValuesAlarm(minVal=0, maxVal=300, alarmText="ERROR") - borderError.receivers = [printCollector] + borderError = BetweenValuesAlarm(minVal=0, maxVal=300, alarmText="ERROR", collector=True) + borderError.receivers = [printCollector, mailSender] return [borderError] + def main(): to = TrendingObjectMock(alarmConfig())