From be7186f5a036887fc2b2f79c5510c94da84bfb53 Mon Sep 17 00:00:00 2001 From: Pawel Dziok Date: Fri, 21 Aug 2015 16:39:48 +0200 Subject: [PATCH] Analyzer runner w/ push notifications - squashed --- module/Dashboard/Module.php | 39 +++- module/Dashboard/config/module.config.php | 27 +++ .../Dashboard/Controller/CliController.php | 27 +++ .../AnalyzerViolation/PushNotifier.php | 190 ++++++++++++++++++ .../Model/Analyzer/AbstractAnalyzer.php | 102 ++++++++++ .../Model/Analyzer/AnalyzerFactory.php | 23 +++ .../Model/Analyzer/AnalyzerInterface.php | 10 + .../Model/Analyzer/AnalyzerResult.php | 116 +++++++++++ .../Model/Analyzer/GraphAnalyzer.php | 78 +++++++ .../Model/Analyzer/NumberAnalyzer.php | 93 +++++++++ .../src/Dashboard/Model/AnalyzerRunner.php | 160 +++++++++++++++ public/scss/widget/_eyeWidget.scss | 1 + 12 files changed, 863 insertions(+), 3 deletions(-) create mode 100644 module/Dashboard/src/Dashboard/Controller/CliController.php create mode 100644 module/Dashboard/src/Dashboard/EventListener/AnalyzerViolation/PushNotifier.php create mode 100644 module/Dashboard/src/Dashboard/Model/Analyzer/AbstractAnalyzer.php create mode 100644 module/Dashboard/src/Dashboard/Model/Analyzer/AnalyzerFactory.php create mode 100644 module/Dashboard/src/Dashboard/Model/Analyzer/AnalyzerInterface.php create mode 100644 module/Dashboard/src/Dashboard/Model/Analyzer/AnalyzerResult.php create mode 100644 module/Dashboard/src/Dashboard/Model/Analyzer/GraphAnalyzer.php create mode 100644 module/Dashboard/src/Dashboard/Model/Analyzer/NumberAnalyzer.php create mode 100644 module/Dashboard/src/Dashboard/Model/AnalyzerRunner.php diff --git a/module/Dashboard/Module.php b/module/Dashboard/Module.php index eda6273..fe02890 100644 --- a/module/Dashboard/Module.php +++ b/module/Dashboard/Module.php @@ -1,11 +1,15 @@ function (ServiceManager $serviceManager) { return new Model\Widget\WidgetFactory($serviceManager->get('WidgetConfig')); }, - 'CacheAdapter' => function ($serviceManager) { + 'CacheAdapter' => function (ServiceManager $serviceManager) { return new Memcached($serviceManager->get('CacheAdapterOptions')); }, - 'CacheAdapterOptions' => function ($serviceManager) { + 'CacheAdapterOptions' => function (ServiceManager $serviceManager) { return new MemcachedOptions($serviceManager->get('Config')['dashboardCache']); }, 'SplunkDaoConfig' => function (ServiceManager $serviceManager) { @@ -123,7 +127,7 @@ public function getServiceConfig() 'SlackDao' => function (ServiceManager $serviceManager) { return new Model\Dao\SlackDao($serviceManager->get('SlackDaoConfig')); }, - 'Parsedown' => function (ServiceManager $serviceManager) { + 'Parsedown' => function () { return new \Parsedown(); }, 'BambooDaoConfig' => function (ServiceManager $serviceManager) { @@ -162,10 +166,27 @@ public function getServiceConfig() 'GraphiteDaoConfig' => function (ServiceManager $serviceManager) { return $serviceManager->get('Config')['GraphiteDao']; }, + 'Dashboard\EventListener\AnalyzerViolation\PushNotifier' => function (ServiceManager $serviceManager) { + return new PushNotifier($serviceManager->get('Config')['push_notification']); + } ), ); } + /** + * {@inheritdoc} + * + * @param EventInterface $e Event + * @return array|void + */ + public function onBootstrap(MvcEvent $e) + { + $eventManager = $e->getApplication()->getEventManager(); + $serviceManager = $e->getApplication()->getServiceManager(); + + $eventManager->attach($serviceManager->get('Dashboard\EventListener\AnalyzerViolation\PushNotifier')); + } + /** * Include all files in modules config/autoload if the directory exists * @param string $configPath Optional path to the configs path @@ -194,4 +215,16 @@ private function autoloadConfigs($configPath = __DIR__) return $config; } + + public function getConsoleUsage(Console $console) + { + return array( + // Describe available commands + 'monitor []' => 'Reset password for a user', + + // Describe expected parameters + array( 'config', 'config name' ), + array( 'widget', 'widget name to listen to'), + ); + } } diff --git a/module/Dashboard/config/module.config.php b/module/Dashboard/config/module.config.php index e44de54..02410db 100644 --- a/module/Dashboard/config/module.config.php +++ b/module/Dashboard/config/module.config.php @@ -45,11 +45,38 @@ ), ), ), + 'console' => [ + 'router' => [ + 'routes' => [ + 'monitor_aggregated' => [ + 'type' => 'simple', + 'options' => [ + 'route' => 'monitor ', + 'defaults' => [ + 'controller' => 'Dashboard\Controller\CliController', + 'action' => 'listenAggregated', + ], + ], + ], + 'monitor' => [ + 'type' => 'simple', + 'options' => [ + 'route' => 'monitor ', + 'defaults' => [ + 'controller' => 'Dashboard\Controller\CliController', + 'action' => 'listen', + ], + ], + ], + ], + ], + ], 'controllers' => array( 'invokables' => array( 'Dashboard\Controller\Dashboard' => 'Dashboard\Controller\DashboardController', 'Dashboard\Controller\LongPollingController' => 'Dashboard\Controller\LongPollingController', 'Dashboard\Controller\EventsApiController' => 'Dashboard\Controller\EventsApiController', + 'Dashboard\Controller\CliController' => 'Dashboard\Controller\CliController', ), ), 'view_manager' => array( diff --git a/module/Dashboard/src/Dashboard/Controller/CliController.php b/module/Dashboard/src/Dashboard/Controller/CliController.php new file mode 100644 index 0000000..ba547b2 --- /dev/null +++ b/module/Dashboard/src/Dashboard/Controller/CliController.php @@ -0,0 +1,27 @@ +params()->fromRoute('configName'); + $dashboardManager = new DashboardManager($configName, $this->serviceLocator); + $analyzer = new AnalyzerRunner($dashboardManager); + $analyzer->runAggregated(); + } + + public function listenAction() + { + $configName = $this->params()->fromRoute('configName'); + $widgetId = $this->params()->fromRoute('widgetId'); + $dashboardManager = new DashboardManager($configName, $this->serviceLocator, $widgetId); + $analyzer = new AnalyzerRunner($dashboardManager); + $analyzer->run($widgetId); + } +} diff --git a/module/Dashboard/src/Dashboard/EventListener/AnalyzerViolation/PushNotifier.php b/module/Dashboard/src/Dashboard/EventListener/AnalyzerViolation/PushNotifier.php new file mode 100644 index 0000000..e7cadd6 --- /dev/null +++ b/module/Dashboard/src/Dashboard/EventListener/AnalyzerViolation/PushNotifier.php @@ -0,0 +1,190 @@ +privateKey = $config['privateKey']; + $this->publicKey = $config['publicKey']; + $this->apiUrl = $config['apiUrl']; + } + + /** + * Attach one or more listeners + * + * Implementors may add an optional $priority argument; the EventManager + * implementation will pass this to the aggregate. + * + * @param EventManagerInterface $events + * + * @return void + */ + public function attach(EventManagerInterface $events) + { + $this->handler = $events->getSharedManager()->attach( + 'Dashboard\Model\AnalyzerRunner', + 'analyzer.violation', + [$this, 'handle'] + ); + } + + /** + * Detach all previously attached listeners + * + * @param EventManagerInterface $events + * + * @return void + */ + public function detach(EventManagerInterface $events) + { + $events->getSharedManager()->detach('Dashboard\Model\AnalyzerRunner', $this->handler); + } + + public function handle(Event $event) + { + $params = $event->getParams(); + /** @var AnalyzerResult $analyzeResult */ + list($analyzeResult, $config) = $params; + + $platforms = $config['analyze']['notify']['platforms']; + $subscriptionIds = $config['analyze']['notify']['subscriptionIds']; + $message = $this->generateMessage($analyzeResult, $subscriptionIds, $platforms); + $request = $this->prepareRequest($message); + + try { + $request->send(); + } catch (ServerErrorResponseException $e) { + //shut up + } + } + + private function generateSignature($method, $endpoint, $timestamp) + { + $hashSource = implode('|', [$method, $this->apiUrl . $endpoint, $this->publicKey, $timestamp]); + + return hash_hmac('sha256', $hashSource, $this->privateKey); + } + + /** + * @param $authSignature + * @param $timestamp + * @return array + */ + private function generateHeaders($authSignature, $timestamp) + { + $headers = [ + "X-VgnoApiAuth-PublicKey" => $this->publicKey, + "X-VgnoApiAuth-Authenticate-Signature" => $authSignature, + "X-VgnoApiAuth-Authenticate-Timestamp" => $timestamp, + "Accept" => "application/json", + "Content-Type" => "application/json", + "User-Agent" => "STP RTM", + ]; + + return $headers; + } + + /** + * @param AnalyzerResult $analyzerResult + * @param $subscriptionIds + * @param $platforms + * @return array + */ + private function generateMessage(AnalyzerResult $analyzerResult, $subscriptionIds, $platforms) + { + $messageTitle = $this->generateMessageTitle($analyzerResult); + $messagePayload = $this->generateMessagePayload($analyzerResult); + + $message = [ + "date" => time(), + "sender" => "STP RTM", + "title" => $messageTitle, + "description" => '', + "sound" => "", + "image" => "", + "incrementor" => 0, + "payload" => $messagePayload, + "subscriptions" => $subscriptionIds, + "platforms" => $platforms, + ]; + + return $message; + } + + /** + * @param $analyzeResult + * @return string + */ + private function generateMessagePayload(AnalyzerResult $analyzeResult) + { + $messagePayload = json_encode([ + 'widgetId' => $analyzeResult->getWidgetId(), + 'app' => $analyzeResult->getApplication(), + 'status' => $analyzeResult->getStatus(), + ]); + + return $messagePayload; + } + + /** + * @param $analyzeResult + * @return string + */ + private function generateMessageTitle(AnalyzerResult $analyzeResult) + { + $messageTitle = sprintf('%s: %s', $analyzeResult->getApplication(), $analyzeResult->getMessage()); + + return $messageTitle; + } + + /** + * @return string + */ + private function generateTimestamp() + { + $timestamp = (new \DateTime())->setTimezone(new \DateTimeZone('Z'))->format('Y-m-d\TH:i:s\Z'); + + return $timestamp; + } + + /** + * @param $message + * @return \Guzzle\Http\Message\EntityEnclosingRequestInterface|\Guzzle\Http\Message\RequestInterface + */ + private function prepareRequest($message) + { + $timestamp = $this->generateTimestamp(); + + $endpoint = 'message'; + $authSignature = $this->generateSignature('POST', $endpoint, $timestamp); + $headers = $this->generateHeaders($authSignature, $timestamp); + + $httpClient = new Client($this->apiUrl); + $request = $httpClient->post($endpoint, $headers, json_encode($message)); + + return $request; + } +} diff --git a/module/Dashboard/src/Dashboard/Model/Analyzer/AbstractAnalyzer.php b/module/Dashboard/src/Dashboard/Model/Analyzer/AbstractAnalyzer.php new file mode 100644 index 0000000..1e6179d --- /dev/null +++ b/module/Dashboard/src/Dashboard/Model/Analyzer/AbstractAnalyzer.php @@ -0,0 +1,102 @@ +widget = $widget; + } + + /** + * @return mixed + */ + abstract public function analyze(); + + /** + * @param $method + * @param $data + * @return float|mixed + */ + protected function calculateResult($method, $data) + { + $count = count($data); + + switch ($method) { + case AbstractAnalyzer::METHOD_ALL: + $result = min($data); + break; + case AbstractAnalyzer::METHOD_ANY: + $result = max($data); + break; + case AbstractAnalyzer::METHOD_AVG: + $result = array_sum($data) / $count; + break; + case AbstractAnalyzer::METHOD_MEDIAN: + $middleIndex = (int)floor($count / 2); + sort($data, SORT_NUMERIC); + $result = $data[$middleIndex]; // assume an odd # of items + // Handle the even case by averaging the middle 2 items + if ($count % 2 == 0) { + $result = ($result + $data[$middleIndex - 1]) / 2; + } + break; + default: + throw new \InvalidArgumentException(sprintf('Unknown method "%s"', $method)); + } + + return $result; + } + + protected function compare($value, $with) + { + if ($this->comparisonMethod == AbstractAnalyzer::HIGHER_IS_BETTER) { + if ($value <= $with) { + return true; + } + + return false; + } + + if ($value >= $with) { + return true; + } + + return false; + } + + protected function generateMessage($title, $status, $result, $suffix, $threshold) + { + return sprintf( + '%s is %s - current value is: %s %s. Threshold is %s', + $title, + $status, + $result, + $suffix, + $threshold + ); + } +} diff --git a/module/Dashboard/src/Dashboard/Model/Analyzer/AnalyzerFactory.php b/module/Dashboard/src/Dashboard/Model/Analyzer/AnalyzerFactory.php new file mode 100644 index 0000000..0ddba6e --- /dev/null +++ b/module/Dashboard/src/Dashboard/Model/Analyzer/AnalyzerFactory.php @@ -0,0 +1,23 @@ +widgetId = $widgetId; + $this->status = $status; + $this->metric = $metric; + $this->message = $message; + $this->application = $application; + } + + /** + * @return string + */ + public function getWidgetId() + { + return $this->widgetId; + } + + /** + * @return string + */ + public function getStatus() + { + return $this->status; + } + + /** + * @return string + */ + public function getMetric() + { + return $this->metric; + } + + /** + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * @return string + */ + public function getApplication() + { + return $this->application; + } + + /** + * @return bool + */ + public function isOk() + { + return $this->getStatus() == self::OK; + } + + /** + * @return bool + */ + public function isCaution() + { + return $this->getStatus() == self::CAUTION; + } + + /** + * @return bool + */ + public function isCritical() + { + return $this->getStatus() == self::CRITICAL; + } + + public function toArray() + { + return [ + 'widgetId' => $this->getWidgetId(), + 'status' => $this->getStatus(), + 'metric' => $this->getMetric(), + 'message' => $this->getMessage(), + 'application' => $this->getApplication(), + ]; + } +} diff --git a/module/Dashboard/src/Dashboard/Model/Analyzer/GraphAnalyzer.php b/module/Dashboard/src/Dashboard/Model/Analyzer/GraphAnalyzer.php new file mode 100644 index 0000000..4d8b83e --- /dev/null +++ b/module/Dashboard/src/Dashboard/Model/Analyzer/GraphAnalyzer.php @@ -0,0 +1,78 @@ +widget->getParams(); + $cfg = $params['analyze']; + $data = $this->gatherImportantData(); + + $method = isset($cfg['method']) + ? $cfg['method'] + : self::METHOD_AVG; + + $result = $this->calculateResult($method, $data); + + $thresholds = $this->widget->getThreshold(); + + $this->comparisonMethod = self::HIGHER_IS_BETTER; + if (!isset($params['thresholdComparator']) || $params['thresholdComparator'] == 'lowerIsBetter') { + $this->comparisonMethod = self::LOWER_IS_BETTER; + } + + $suffix = isset($params['valueSuffix']) ? $params['valueSuffix'] : ''; + + $title = $params['title']; + if ($this->compare($result, $thresholds['critical-value'])) { + //omg omg omg! critical + $status = AnalyzerResult::CRITICAL; + $message = $this->generateMessage($title, 'CRITICAL', $result, $suffix, $thresholds['critical-value']); + } elseif ($this->compare($result, $thresholds['caution-value'])) { + $status = AnalyzerResult::CAUTION; + $message = $this->generateMessage($title, 'CAUTION', $result, $suffix, $thresholds['caution-value']); + } else { + $status = AnalyzerResult::OK; + $message = sprintf('%s is OK - current value is: %s %s', $title, $result, $suffix); + } + + $message = rtrim($message); + + return new AnalyzerResult($this->widget->getId(), $status, $params['metric'], $message, $params['app']); + } + + /** + * @return array + */ + private function gatherImportantData() + { + $data = $this->widget->fetchData(); + $params = $this->widget->getParams(); + + $cfg = $params['analyze']; + + $currentDate = new \DateTime(); + $since = $currentDate->sub(\DateInterval::createFromDateString($cfg['last']))->getTimestamp(); + + $importantData = []; + for ($i = count($data) - 1; $i >= 0; $i--) { + $possibleImportant =& $data[$i]; + if (($possibleImportant['x'] / 1000) - 7200 > $since) { + $importantData[] = $possibleImportant['y']; + } + } + + return $importantData; + } +} diff --git a/module/Dashboard/src/Dashboard/Model/Analyzer/NumberAnalyzer.php b/module/Dashboard/src/Dashboard/Model/Analyzer/NumberAnalyzer.php new file mode 100644 index 0000000..479acb0 --- /dev/null +++ b/module/Dashboard/src/Dashboard/Model/Analyzer/NumberAnalyzer.php @@ -0,0 +1,93 @@ +getParam('analyze'); + + $secondsToAnalyze = $this->intervalToSeconds($cfg['last']); + $frequency = isset($cfg['each']) + ? $this->intervalToSeconds($cfg['each']) + : 60; + $this->samples = ceil($secondsToAnalyze / $frequency); + } + + public function analyze() + { + $params = $this->widget->getParams(); + $cfg = $params['analyze']; + $data = $this->gatherImportantData(); + + $method = isset($cfg['method']) + ? $cfg['method'] + : self::METHOD_AVG; + + $result = $this->calculateResult($method, $data); + + $thresholds = $this->widget->getThreshold(); + + $this->comparisonMethod = self::HIGHER_IS_BETTER; + if (!isset($params['thresholdComparator']) || $params['thresholdComparator'] == 'lowerIsBetter') { + $this->comparisonMethod = self::LOWER_IS_BETTER; + } + + $suffix = isset($params['valueSuffix']) ? $params['valueSuffix'] : ''; + + $title = $params['title']; + if ($this->compare($result, $thresholds['critical-value'])) { + //omg omg omg! critical + $status = AnalyzerResult::CRITICAL; + $message = $this->generateMessage($title, 'CRITICAL', $result, $suffix, $thresholds['critical-value']); + } elseif ($this->compare($result, $thresholds['caution-value'])) { + $status = AnalyzerResult::CAUTION; + $message = $this->generateMessage($title, 'CAUTION', $result, $suffix, $thresholds['caution-value']); + } else { + $status = AnalyzerResult::OK; + $message = sprintf('%s is OK - current value is: %s %s', $title, $result, $suffix); + } + + $message = rtrim($message); + + $application = isset($params['app']) ? $params['app'] : ''; + + return new AnalyzerResult($this->widget->getId(), $status, $params['metric'], $message, $application); + } + + /** + * @return array + */ + private function gatherImportantData() + { + $this->archive[] = $this->widget->fetchData(); + + if (count($this->archive) > $this->samples) { + array_shift($this->archive); + } + + return $this->archive; + } + + private function intervalToSeconds($value) + { + $interval = \DateInterval::createFromDateString($value); + + return ($interval->y * 365 * 24 * 60 * 60) + + ($interval->m * 30 * 24 * 60 * 60) + + ($interval->d * 24 * 60 * 60) + + ($interval->h * 60 * 60) + + ($interval->i * 60) + + $interval->s; + } +} diff --git a/module/Dashboard/src/Dashboard/Model/AnalyzerRunner.php b/module/Dashboard/src/Dashboard/Model/AnalyzerRunner.php new file mode 100644 index 0000000..5dfd222 --- /dev/null +++ b/module/Dashboard/src/Dashboard/Model/AnalyzerRunner.php @@ -0,0 +1,160 @@ +dashboardManager = $dashboardManager; + /** @var EventManagerInterface $eventManager */ + $eventManager = $dashboardManager->getServiceLocator()->get('eventmanager'); + $this->setEventManager($eventManager); + } + + public function runAggregated() + { + $widgets = $this->dashboardManager->getWidgets(); + + $filteredWidgets = []; + /** @var AbstractWidget $widget */ + foreach ($widgets as $widget) { + $thresholds = $widget->getThreshold(); + $thresholds = array_filter($thresholds); + + if (!$thresholds) { + $this->logLn('[Master] Skipping "%s" due to lack of thresholds', $widget->getId()); + } elseif (!$widget->getParam('analyze')) { + $this->logLn('[Master] Skipping "%s" due to lack of analyzer config', $widget->getId()); + } else { + $filteredWidgets[] = $widget; + } + } + + /** @var Process[] $processes */ + foreach ($filteredWidgets as $widget) { + $this->logLn( + '[Master] Monitoring "%s" with:%s * thresholds: %s%s * analyzer config: %s', + $widget->getId(), + PHP_EOL, + json_encode($widget->getThreshold()), + PHP_EOL, + json_encode($widget->getParam('analyze')) + ); + + $cmd = $this->prepareCommand($widget); + $process = new Process($cmd); + $process->start(function ($type, $message) use ($widget) { + $this->log('[%s] %s', $widget->getId(), $message); + }); + + $this->processes[$widget->getId()] = $process; + } + + while (true) { + sleep(1); + + foreach ($this->processes as $widgetId => $process) { + if (!$process->isRunning()) { + $this->logLn(sprintf('%s: %s', $widgetId, $process->getStatus())); + $process->restart(function ($type, $message) use ($widget) { + $this->log('[%s] %s', $widget->getId(), $message); + }); + } + } + } + } + + public function run($widgetId) + { + /** @var AbstractWidget $widget */ + $widget = $this->dashboardManager->getWidget($widgetId); + $factory = new AnalyzerFactory(); + + $analyzer = $factory->createFor($widget); + + $cfg = $widget->getParam('analyze'); + $sleepFor = isset($cfg['each']) + ? \DateInterval::createFromDateString($cfg['each'])->format('%s') + : 60; + + $this->logLn('Ready!'); + + while (true) { + $analyzeResult = $analyzer->analyze(); + + if (!$analyzeResult->isOK()) { + $this->logLn( + 'Violation found @ %s: %s', + $analyzeResult->getApplication(), + $analyzeResult->getMessage() + ); + $this->events->trigger('analyzer.violation', $this, [$analyzeResult, $widget->getParams()]); + } + + sleep($sleepFor); + } + } + + /** + * @param $message + */ + private function log($message) + { + if (func_num_args() > 1) { + $message = vsprintf($message, array_slice(func_get_args(), 1)); + } + + file_put_contents('php://stdout', $message); + } + + /** + * @param $message + */ + private function logLn($message) + { + $message .= PHP_EOL; + + if (func_num_args() > 1) { + call_user_func_array([$this, 'log'], array_merge([$message], array_slice(func_get_args(), 1))); + } else { + $this->log($message); + } + } + + /** + * @param $widget + * @return string + */ + private function prepareCommand(AbstractWidget $widget) + { + $argv = $_SERVER['argv']; + $cmd = sprintf('php %s %s %s %s', $argv[0], $argv[1], $argv[2], $widget->getId()); + + return $cmd; + } + + public function __destruct() + { + foreach ($this->processes as $name => $process) { + $this->logLn('Killing child process: %s', $name); + $process->stop(); + } + } +} diff --git a/public/scss/widget/_eyeWidget.scss b/public/scss/widget/_eyeWidget.scss index 8a8521b..1a25dc7 100644 --- a/public/scss/widget/_eyeWidget.scss +++ b/public/scss/widget/_eyeWidget.scss @@ -2,6 +2,7 @@ // Widget-messages styles // ---------------------------------------------------------------------------- #dashboardBody .widget.EyeWidget { + font-size: 1.2rem; overflow: hidden; .process {