├── README.md ├── application ├── clicommands │ └── MailCommand.php ├── controllers │ └── ReportController.php ├── forms │ └── ReportForm.php └── views │ ├── helpers │ └── SlaValue.php │ └── scripts │ ├── report │ └── show.phtml │ └── reports │ ├── service-sla.phtml │ └── sla.phtml ├── configuration.php ├── doc └── README.md ├── library └── Reporting │ ├── File │ └── Csv.php │ ├── Ido.php │ ├── Mail.php │ ├── ProvidedHook │ └── Monitoring │ │ ├── DataviewExtension.php │ │ └── IdoQueryExtension.php │ ├── Report │ ├── HostslaReport.php │ ├── IdoReport.php │ ├── ServiceslaReport.php │ └── SlaReport.php │ ├── Timeframe.php │ ├── Timeframes.php │ └── Web │ ├── Controller.php │ ├── Form │ ├── CsrfToken.php │ ├── FormLoader.php │ └── QuickForm.php │ └── Hook │ └── ReportHook.php ├── module.info ├── public └── css │ └── module.less ├── run.php └── schema ├── README.slaperiod ├── get_sladetail-procedure.sql ├── icinga_sla.sql ├── refresh_outofslaperiods-procedure.sql ├── refresh_slaperiods-procedure.sql └── slaperiod-schema.sql /README.md: -------------------------------------------------------------------------------- 1 | # Reporting 2 | 3 | Hint: this is a prototype at a very early stage, expect it to break any 4 | time without prior announcement 5 | 6 | ## Configure your own timeframes 7 | 8 | Place this or a similar config in your `/modules/reporting/timeframes.ini`: 9 | ```ini 10 | [4_hours] 11 | title = "4 Stunden" 12 | start = "-4hours" 13 | end = "now" 14 | 15 | [25_hours] 16 | title = "25 Stunden" 17 | start = "-25hours" 18 | end = "now" 19 | 20 | [one_week] 21 | title = "Eine Woche" 22 | start = "-1week" 23 | end = "now" 24 | 25 | [one_month] 26 | title = "Ein Monat" 27 | start = "-1month" 28 | end = "now" 29 | 30 | [one_year] 31 | title = "Ein Jahr" 32 | start = "-1year" 33 | end = "now" 34 | 35 | [current_week] 36 | title = "KW {START_WEEK} (bis jetzt)" 37 | start = "last monday 00:00:00" 38 | end = "now" 39 | 40 | [last_week] 41 | title = "KW{START_WEEK}/{START_YEAR}" 42 | start = "last monday -1week 00:00:00" 43 | end = "last sunday 23:59:59" 44 | 45 | [minus2_week] 46 | title = "KW{START_WEEK}/{START_YEAR}" 47 | start = "last monday -2week 00:00:00" 48 | end = "last sunday -1week 23:59:59" 49 | 50 | [minus3_week] 51 | title = "KW{START_WEEK}/{START_YEAR}" 52 | start = "last monday -3week 00:00:00" 53 | end = "last sunday -2week 23:59:59" 54 | 55 | [minus4_week] 56 | title = "KW{START_WEEK}/{START_YEAR}" 57 | start = "last monday -4week 00:00:00" 58 | end = "last sunday -3week 23:59:59" 59 | 60 | [current_month] 61 | title = "{START_MONTHNAME} (bis jetzt)" 62 | start = "first day of this month 00:00:00" 63 | end = "now" 64 | 65 | [last_month] 66 | title = "{START_MONTHNAME}" 67 | start = "first day of last month 00:00:00" 68 | end = "last day of last month 23:59:59" 69 | 70 | [minus2_month] 71 | title = "{START_MONTHNAME}" 72 | start = "first day of last month -1month 00:00:00" 73 | end = "last day of last month -1month 23:59:59" 74 | 75 | [minus3_month] 76 | title = "{START_MONTHNAME}" 77 | start = "first day of last month -2month 00:00:00" 78 | end = "last day of last month -2month 23:59:59" 79 | 80 | [minus4_month] 81 | title = "{START_MONTHNAME}" 82 | start = "first day of last month -3month 00:00:00" 83 | end = "last day of last month -3month 23:59:59" 84 | 85 | [minus5_month] 86 | title = "{START_MONTHNAME}" 87 | start = "first day of last month -4month 00:00:00" 88 | end = "last day of last month -4month 23:59:59" 89 | 90 | [current_year] 91 | title = "{START_YEAR} (Dieses Jahr)" 92 | start = "first day of January this year 00:00:00" 93 | end = "now" 94 | 95 | [last_year] 96 | title = "{START_YEAR} (Letztes Jahr)" 97 | start = "first day of January last year 00:00:00" 98 | end = "last day of December last year 23:59:59" 99 | 100 | [minus2_year] 101 | title = "{START_YEAR}" 102 | start = "first day of January last year -1year 00:00:00" 103 | end = "last day of December last year -1year 23:59:59" 104 | 105 | [minus3_year] 106 | title = "{START_YEAR}" 107 | start = "first day of January last year -2year 00:00:00" 108 | end = "last day of December last year -2year 23:59:59" 109 | ``` 110 | 111 | -------------------------------------------------------------------------------- /application/clicommands/MailCommand.php: -------------------------------------------------------------------------------- 1 | loadForm('report'); 17 | $form->handleValues(array( 18 | 'report' => 'Icinga\\Module\\Reporting\\Report\\HostslaReport', 19 | 'timeframes' => array('current_week', 'last_week', 'minus2_week', 'minus3_week'), 20 | 'hostgroup' => 'Some group', 21 | 'Submit' => 'Submit' 22 | )); 23 | 24 | $report = $form->getReport(); 25 | $from = $this->params->shift('from'); 26 | $to = $this->params->shift('to'); 27 | 28 | Mail::create() 29 | ->setFrom($from) 30 | ->addTo($to) 31 | ->setSubject('Report "' . $report->getName() . '"') 32 | ->addHtmlImage('/tmp/graph.png') 33 | ->setBodyText("Irgendwas\n========\n\nNix zu sagen\n") 34 | ->setBodyHtml( 35 | '

Irgendwas - Servus

' 36 | . '

Mailst mir bitte einen Screenshot dieser Mail?

' 37 | . $this->renderReport($report) 38 | . '' 39 | . '

Vielen Dank - Tom

' 40 | ) 41 | ->send(); 42 | } 43 | 44 | protected function renderReport($report) 45 | { 46 | return $this->fixHtml($report->render($this->viewRenderer()->view)); 47 | } 48 | 49 | protected function viewRenderer() 50 | { 51 | $app = Icinga::app(); 52 | $view = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer'); 53 | $view->setView(new View()); 54 | $view->view->addHelperPath($app->getApplicationDir('/views/helpers')); 55 | $view->view->addHelperPath($this->Module()->getApplicationDir() .'/views/helpers'); 56 | $view->view->addBasePath($this->Module()->getApplicationDir() .'/views'); 57 | $view->view->setEncoding('UTF-8'); 58 | return $view; 59 | } 60 | 61 | protected function fixHtml($html) 62 | { 63 | $html = preg_replace('/ class="ok"/', ' class="ok" style="background-color: #44BB77; color: white;"', $html); 64 | $html = '
' 65 | . $html 66 | . '
'; 67 | 68 | // $html = preg_replace('/\([^<]+)<\/a\>/', '\1', $html); 70 | return $html; 71 | } 72 | 73 | protected function loadForm($name) 74 | { 75 | return FormLoader::load($name, $this->Module()); 76 | } 77 | 78 | protected function Module() 79 | { 80 | return Icinga::app()->getModuleManager()->getModule('reporting'); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /application/controllers/ReportController.php: -------------------------------------------------------------------------------- 1 | view->reportForm = $this->loadForm('report')->handleRequest(); 14 | if ($this->sendDownloads($form)) { 15 | return; 16 | } 17 | 18 | $this->view->report = $form->getReport(); 19 | 20 | $this->view->tabs->add('reporting', array( 21 | 'label' => 'Reporting', 22 | 'url' => $this->getRequest()->getUrl() 23 | ))->activate('reporting'); 24 | } 25 | 26 | protected function sendDownloads(ReportForm $form) 27 | { 28 | $url = clone($this->getRequest()->getUrl()); 29 | if (! $url->shift('download')) { 30 | return false; 31 | } 32 | 33 | $urlParams = $url->getParams(); 34 | $params = array(); 35 | foreach (array_keys($urlParams->toArray(false)) as $key) { 36 | if ($key === 'timeframes') { 37 | $params[$key] = $urlParams->getValues($key); 38 | } else { 39 | $params[$key] = $urlParams->get($key); 40 | } 41 | } 42 | 43 | $report = $form->loadReportByName( 44 | $params['report'] 45 | )->setValues($params); 46 | 47 | $report->getCsv()->send($this); 48 | 49 | return true; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /application/forms/ReportForm.php: -------------------------------------------------------------------------------- 1 | enumReports(); 19 | 20 | $this->addElement('select', 'report', array( 21 | 'label' => $this->translate('Report Type'), 22 | 'multiOptions' => $this->optionalEnum($availableReports), 23 | 'class' => 'autosubmit', 24 | )); 25 | 26 | $this->setSubmitLabel($this->translate('Next')); 27 | 28 | if (! $this->hasBeenSent()) { 29 | return; 30 | } 31 | $post = $this->getRequest()->getPost(); 32 | 33 | if (! $this->isValidPartial($post)) { 34 | return; 35 | } 36 | $class = $this->getValue('report'); 37 | if (! $class) { 38 | return; 39 | } 40 | 41 | $report = new $class; 42 | $report->addFormElements($this); 43 | if ($this->isValidPartial($this->getRequest()->getPost())) { 44 | foreach ($this->getElements() as $el) { 45 | if ($el->isRequired() && ! isset($post[$el->getName()])) { 46 | return; 47 | } 48 | } 49 | $this->report = $report->setValues($this->getValues()); 50 | } 51 | 52 | $this->setSubmitLabel($this->translate('Generate')); 53 | } 54 | 55 | public function getDownloadUrl() 56 | { 57 | $values = $this->getValues(); 58 | if (array_key_exists('report', $values)) { 59 | $values['report'] = $this->getReportNameForClass($values['report']); 60 | } 61 | 62 | if (array_key_exists('timeframes', $values)) { 63 | $timeframes = $values['timeframes']; 64 | unset($values['timeframes']); 65 | } else { 66 | $timeframes = null; 67 | } 68 | 69 | $url = Url::fromPath($this->getAction(), $values); 70 | 71 | if ($timeframes) { 72 | $url->getParams() 73 | ->addValues('timeframes', $timeframes) 74 | ->add('download'); 75 | } 76 | 77 | return $url; 78 | } 79 | 80 | public function getReport() 81 | { 82 | return $this->report; 83 | } 84 | 85 | public function onSuccess() 86 | { 87 | // No redirect 88 | } 89 | 90 | protected function getReportNameForClass($classname) 91 | { 92 | $enum = $this->enumReports(); 93 | return $enum[$classname]; 94 | } 95 | 96 | /** 97 | * @param $name 98 | * @return \Icinga\Module\Reporting\Web\Hook\ReportHook 99 | */ 100 | public function loadReportByName($name) 101 | { 102 | $enum = $this->enumReports(); 103 | $enum = array_flip($enum); 104 | return new $enum[$name]; 105 | } 106 | 107 | protected function enumReports() 108 | { 109 | $hooks = Hook::all('Reporting\\Report'); 110 | $enum = array(); 111 | foreach ($hooks as $hook) { 112 | $enum[get_class($hook)] = $hook->getName(); 113 | } 114 | 115 | return $enum; 116 | } 117 | 118 | protected function availableReports() 119 | { 120 | return array( 121 | 'hostsla' => 'Host Availability', 122 | 'graphs' => 'KPI Graphs', 123 | ); 124 | } 125 | 126 | protected function enumHostgroups() 127 | { 128 | return $this->db->enumHostgroups(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /application/views/helpers/SlaValue.php: -------------------------------------------------------------------------------- 1 | setQueryString($timeframe->toFilter()->toQueryString()); 22 | $params = $url->getParams(); 23 | if ($service === null) { 24 | $params->unshift('object_type', 'host'); 25 | $params->unshift('host_name', $host); 26 | } else { 27 | $params->unshift('service_description', $service); 28 | $params->unshift('host_name', $host); 29 | } 30 | 31 | $params->add('limit', 100); 32 | 33 | if ($this->linkToHistory()) { 34 | return $this->view->qlink( 35 | sprintf('%.3f %%', $value), 36 | $url, 37 | null, 38 | ['title' => sprintf('%f', $value)] 39 | ); 40 | } else { 41 | return $this->view->qlink( 42 | sprintf('%.3f %%', $value), 43 | '#', 44 | null, 45 | ['title' => sprintf('%f', $value)] 46 | ); 47 | } 48 | } 49 | 50 | protected static function linkToHistory() 51 | { 52 | if (self::$linkToHistory === null) { 53 | self::$linkToHistory = Config::module('reporting') 54 | ->get('ui', 'link_to_history', 'yes') === 'yes'; 55 | } 56 | 57 | return self::$linkToHistory; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /application/views/scripts/report/show.phtml: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 |

translate('Reporting') ?>

8 | 9 |
10 |
11 | report): ?> 12 | report->render($this) ?> 13 | 14 |

translate('Welcome to the reporting module') ?>

15 | 16 | icon('barchart') ?> 17 | 18 | translate('Please choose one of the available reports') ?> 19 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /application/views/scripts/reports/service-sla.phtml: -------------------------------------------------------------------------------- 1 | 2 | qlink( 3 | 'Download', 4 | $form->getDownloadUrl(), 5 | null, 6 | array( 7 | 'icon' => 'download', 8 | 'target' => '_blank' 9 | ) 10 | ) ?> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 39 | $timeframe): ?> 40 | 45 | 46 | 47 | 48 | 49 |
translate('Service availability for the chosen timeperiods') ?>
translate('Service availability') ?>escape($timeframe->getTitle()) ?>
31 | qlink($row->hostname, 'monitoring/host/show', [ 32 | 'host' => $row->hostname 33 | ]) ?>: 34 | qlink($row->servicename, 'monitoring/service/show', [ 35 | 'host' => $row->hostname, 36 | 'service' => $row->servicename 37 | ]) ?> 38 | slaValue($row->$alias, $timeframe, $row->hostname, $row->servicename) 43 | 44 | ?>
50 | -------------------------------------------------------------------------------- /application/views/scripts/reports/sla.phtml: -------------------------------------------------------------------------------- 1 | 2 | qlink( 3 | 'Download', 4 | $form->getDownloadUrl(), 5 | null, 6 | array( 7 | 'icon' => 'download', 8 | 'target' => '_blank' 9 | ) 10 | ) ?> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | $timeframe): ?> 32 | 37 | 38 | 39 | 40 | 41 |
translate('Host availability for the chosen hostgroup and timeperiods') ?>
translate('Host availability') ?>escape($timeframe->getTitle()) ?>
qlink($row->hostname, 'monitoring/host/show', ['host' => $row->hostname]) ?>slaValue($row->$alias, $timeframe, $row->hostname) 35 | 36 | ?>
42 | -------------------------------------------------------------------------------- /configuration.php: -------------------------------------------------------------------------------- 1 | menuSection('Reporting')->add(N_('Reports'), array( 4 | 'url' => 'reporting/report/show', 5 | )); 6 | 7 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /library/Reporting/File/Csv.php: -------------------------------------------------------------------------------- 1 | data = $data; 16 | } 17 | 18 | public static function create($data) 19 | { 20 | return new static($data); 21 | } 22 | 23 | public function setFilename($filename) 24 | { 25 | $this->filename = $filename; 26 | return $this; 27 | } 28 | 29 | public function send(Controller $controller) 30 | { 31 | $controller->getHelper('layout')->disableLayout(); 32 | $controller->getHelper('viewRenderer')->setNoRender(true); 33 | $response = $controller->getResponse(); 34 | $out = fopen('php://output', 'w'); 35 | $response->setHeader( 36 | 'Content-Disposition', 37 | 'attachment; filename="' . $this->filename . '"' 38 | ); 39 | 40 | $response->setHeader('Content-type', 'text/csv'); 41 | foreach ($this->data as $row) { 42 | fputcsv($out, (array) $row, ';'); 43 | } 44 | fclose($out); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /library/Reporting/Ido.php: -------------------------------------------------------------------------------- 1 | db === null) { 22 | $this->db = $this->monitoring()->getResource()->getDbAdapter(); 23 | } 24 | 25 | return $this->db; 26 | } 27 | 28 | public function enumHostgroups(Filter $filter = null) 29 | { 30 | $query = $this->monitoring()->select() 31 | ->from('hostgroup', array('hostgroup_name', 'hostgroup_alias')) 32 | ->order('hostgroup_alias'); 33 | 34 | $this->applyObjectRestrictions($query); 35 | 36 | return array(null => '- please choose -') + $query->getQuery()->fetchPairs(); 37 | } 38 | 39 | public function enumServicegroups(Filter $filter = null) 40 | { 41 | $query = $this->monitoring()->select() 42 | ->from('servicegroup', array('servicegroup_name', 'servicegroup_alias')) 43 | ->order('servicegroup_alias'); 44 | 45 | $this->applyObjectRestrictions($query); 46 | 47 | return array(null => '- please choose -') + $query->getQuery()->fetchPairs(); 48 | } 49 | 50 | public function enumDistinctServicesForHostgroup($hostgroup) 51 | { 52 | $query = $this->monitoring()->select() 53 | ->from('serviceStatus', array('service_description', 'service_display_name')) 54 | ->where('hostgroup', $hostgroup) 55 | ->order('service_display_name'); 56 | $this->applyObjectRestrictions($query); 57 | 58 | return array('_HOST_' => 'Host Check') + $query->getQuery()->distinct()->fetchPairs(); 59 | } 60 | 61 | protected function applyObjectRestrictions($query) 62 | { 63 | if (Icinga::app()->isCli()) { 64 | return $query; 65 | } 66 | 67 | $restrictions = Filter::matchAny(); 68 | foreach (Auth::getInstance()->getRestrictions('monitoring/filter/objects') as $filter) { 69 | $restrictions->addFilter(Filter::fromQueryString($filter)); 70 | } 71 | 72 | return $query->applyFilter($restrictions); 73 | } 74 | 75 | public function monitoring() 76 | { 77 | if ($this->monitoring === null) { 78 | $this->monitoring = Backend::instance(); 79 | } 80 | 81 | return $this->monitoring; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /library/Reporting/Mail.php: -------------------------------------------------------------------------------- 1 | htmlImages) > 0) { 33 | $pre = '/(\]*src=")'; 34 | $post = '"/'; 35 | foreach ($this->htmlImages as $name => $img) { 36 | $preg = $pre . preg_quote($name, '/') . $post; 37 | $html = preg_replace($preg, '$1cid:' . $img->id . '"', $html); 38 | } 39 | 40 | $this->setType(Zend_Mime::MULTIPART_RELATED); 41 | } 42 | 43 | if ($charset === null) { 44 | $charset = 'utf8'; 45 | } 46 | 47 | return parent::setBodyHtml($html, $charset, $encoding); 48 | } 49 | 50 | public function addHtmlImage( 51 | $file, 52 | $isFilename = true, 53 | $mimetype = null, 54 | $name = null 55 | ) { 56 | $p_suff = '~\.(jpg|jpeg|png|gif)$~i'; 57 | 58 | $name = $this->uniqueImageFilename($name, $file, $isFilename); 59 | 60 | if ($isFilename) { 61 | $this->assertFileExists($file); 62 | $file = file_get_contents($file); 63 | } 64 | 65 | if (preg_match($p_suff, $name, $match)) { 66 | if ($mimetype === null) { 67 | $mimetype = 'image/' . $match[1]; 68 | } 69 | } else { 70 | if (preg_match($p_suff, $mimetype, $match)) { 71 | $name .= '.' . $match[1]; 72 | } 73 | } 74 | 75 | if (isset($this->htmlImages[$name])) return false; 76 | $key = md5($file); 77 | $img = $this->createAttachment($file); 78 | $img->type = $mimetype; 79 | $img->filename = $name; 80 | $img->id = $key; 81 | $img->disposition = Zend_Mime::DISPOSITION_INLINE; 82 | $img->encoding = Zend_Mime::ENCODING_BASE64; 83 | $this->htmlImages[$name] = $img; 84 | 85 | return $this; 86 | } 87 | 88 | public function attachFile( 89 | $file, 90 | $isFilename = true, 91 | $name = null, 92 | $mimetype = 'application/octet-stream' 93 | ) { 94 | if ($name) { 95 | $filename = $name; 96 | } else { 97 | $filename = 'image' . (count($this->htmlImages) + 1); 98 | } 99 | 100 | if ($isFilename) { 101 | $this->assertFileExists($file); 102 | 103 | if ($name === null) { 104 | $filename = basename($file); 105 | } 106 | $file = file_get_contents($file); 107 | } else { 108 | if ($name === null) { 109 | throw new ProgrammingError( 110 | 'Filename is required when attaching blobs to mails' 111 | ); 112 | } 113 | } 114 | 115 | $att = $this->createAttachment($file); 116 | $att->type = $mimetype; 117 | $att->filename = $filename; 118 | 119 | return $this; 120 | } 121 | 122 | public function send($transport = null) 123 | { 124 | if ($transport === null) { 125 | $transport = new Zend_Mail_Transport_Smtp('localhost'); 126 | } 127 | 128 | parent::send($transport); 129 | } 130 | 131 | protected function uniqueImageFileName($name, $filename, $isFilename) 132 | { 133 | if ($name === null) { 134 | if ($isFilename) { 135 | $filename = basename($filename); 136 | } else { 137 | $filename = 'image' . (count($this->htmlImages) + 1); 138 | } 139 | } else { 140 | $filename = $name; 141 | } 142 | 143 | return $filename; 144 | } 145 | 146 | protected function assertFileExists($file) 147 | { 148 | if (! is_readable($file)) { 149 | throw new ProgrammingError('Unable to open "%s"', $file); 150 | } 151 | 152 | return $this; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /library/Reporting/ProvidedHook/Monitoring/DataviewExtension.php: -------------------------------------------------------------------------------- 1 | array( 18 | 'sla_timeperiod_name' => 'hostsla.sla_timeperiod_name', 19 | 'sla_last_seen' => 'hostsla.sla_last_seen', 20 | 'host_in_slatime' => 'CASE WHEN (hostsla.sla_last_seen IS NULL) OR (hostsla.sla_last_seen > hs.last_hard_state_change) THEN 1 ELSE 0 END', 21 | 'servicehost_in_slatime' => 'CASE WHEN (hostsla.sla_last_seen IS NULL) OR (hostsla.sla_last_seen > ss.last_hard_state_change) THEN 1 ELSE 0 END' 22 | ) 23 | ); 24 | } 25 | } 26 | 27 | public function joinVirtualTable(IdoQuery $query, $virtualTable) 28 | { 29 | if ($virtualTable === 'hostsla') { 30 | if ($query instanceof HoststatusQuery) { 31 | $this->joinHostsla($query, 'ho.object_id'); 32 | } elseif ($query instanceof ServicestatusQuery) { 33 | $this->joinHostsla($query, 's.host_object_id'); 34 | } 35 | } 36 | } 37 | 38 | protected function joinHostsla(IdoQuery $query, $joinOn) 39 | { 40 | $db = $query->getDatasource()->getDbAdapter(); 41 | $prefix = 'icinga_'; 42 | 43 | $sla_periods = $db->select()->from( 44 | array('cv' => $prefix . 'customvariablestatus'), 45 | array('tp_name' => "CONCAT('sla', cv.varvalue) COLLATE latin1_general_cs") 46 | )->join( 47 | array('o' => $prefix . 'objects'), 48 | "o.object_id = cv.object_id AND o.is_active = 1 AND o.objecttype_id = 1 AND LOWER(varname) = 'sla_id'", 49 | array() 50 | )->group('cv.varvalue'); 51 | 52 | $hostsla = $db->select()->from( 53 | array('tpo' => $prefix . 'objects'), 54 | array( 55 | 'sla_timeperiod_name' => 'tpcv.tp_name', 56 | 'sla_last_seen' => 'CASE WHEN MAX(slp.end_time) > NOW() THEN NOW() ELSE MAX(slp.end_time) END' 57 | ) 58 | )->join( 59 | array('tpcv' => $sla_periods), 60 | 'tpo.name1 = tpcv.tp_name AND tpo.objecttype_id = 9 AND tpo.is_active = 1', 61 | array() 62 | )->join( 63 | array('slp' => $prefix . 'sla_periods'), 64 | 'slp.timeperiod_object_id = tpo.object_id', 65 | array() 66 | 67 | )->where('slp.start_time < NOW()') 68 | ->group('timeperiod_object_id'); 69 | 70 | $query->joinLeft( 71 | array('sla_cv' => $prefix . 'customvariablestatus'), 72 | "sla_cv.object_id = $joinOn AND LOWER(sla_cv.varname) = 'sla_id'", 73 | array() 74 | )->joinLeft( 75 | array( 'hostsla' => $hostsla ), 76 | "hostsla.sla_timeperiod_name = CONCAT('sla', sla_cv.varvalue) COLLATE latin1_general_cs", 77 | array() 78 | ); 79 | 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /library/Reporting/Report/HostslaReport.php: -------------------------------------------------------------------------------- 1 | addTimeframesElement($form); 17 | $form->addElement('select', 'hostgroup', array( 18 | 'label' => $form->translate('Hostgroup'), 19 | 'multiOptions' => $form->optionalEnum($this->ido()->enumHostgroups()), 20 | 'class' => 'autosubmit', 21 | 'required' => true 22 | )); 23 | } 24 | 25 | public function getResult() 26 | { 27 | return $this->ido()->db()->fetchAll($this->getQuery()); 28 | } 29 | 30 | public function getQuery() 31 | { 32 | $hostgroup = $this->getValue('hostgroup'); 33 | $db = $this->ido()->db(); 34 | 35 | $query = $db->select()->from( 36 | array('ho' => 'icinga_objects'), 37 | array() 38 | )->join( 39 | array('hgm' => 'icinga_hostgroup_members'), 40 | 'ho.object_id = hgm.host_object_id', 41 | array() 42 | )->join( 43 | array('hg' => 'icinga_hostgroups'), 44 | 'hg.hostgroup_id = hgm.hostgroup_id', 45 | array() 46 | )->join( 47 | array('hgo' => 'icinga_objects'), 48 | 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1', 49 | array() 50 | ); 51 | 52 | $columns = array('hostname' => 'ho.name1'); 53 | $this->addSlaColumnsToQuery($query, 'ho.object_id', $columns); 54 | 55 | $query->where('hgo.name1 = ?', $hostgroup) 56 | ->where('ho.is_active = 1'); 57 | 58 | $query->order('hostname'); 59 | 60 | return $query; 61 | } 62 | 63 | protected function getMainCsvHeaders() 64 | { 65 | return ['Host']; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /library/Reporting/Report/IdoReport.php: -------------------------------------------------------------------------------- 1 | ido === null) { 18 | $this->ido = new Ido(); 19 | } 20 | 21 | return $this->ido; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /library/Reporting/Report/ServiceslaReport.php: -------------------------------------------------------------------------------- 1 | addTimeframesElement($form); 26 | $form->addElement('select', 'hostgroup', [ 27 | 'label' => $form->translate('Hostgroup'), 28 | 'multiOptions' => $form->optionalEnum($this->ido()->enumHostgroups()), 29 | 'class' => 'autosubmit', 30 | 'required' => false 31 | ]); 32 | $form->addElement('select', 'servicegroup', [ 33 | 'label' => $form->translate('Servicegroup'), 34 | 'multiOptions' => $form->optionalEnum($this->ido()->enumServicegroups()), 35 | 'class' => 'autosubmit', 36 | 'required' => false 37 | ]); 38 | $form->addElement('select', 'limit', [ 39 | 'label' => $form->translate('Limit Result'), 40 | 'multiOptions' => [ 41 | '50' => '50', 42 | '100' => '100', 43 | '500' => '500', 44 | '2000' => '2000', 45 | '10000' => '10000 (expensive)', 46 | ], 47 | 'class' => 'autosubmit', 48 | 'required' => false 49 | ]); 50 | } 51 | 52 | /** 53 | * @return mixed 54 | */ 55 | public function getResult() 56 | { 57 | return $this->ido()->db()->fetchAll($this->getQuery()); 58 | } 59 | 60 | public function getQuery() 61 | { 62 | $hostgroup = $this->getValue('hostgroup'); 63 | $servicegroup = $this->getValue('servicegroup'); 64 | $db = $this->ido()->db(); 65 | 66 | $query = $db->select()->from( 67 | ['so' => 'icinga_objects'], 68 | [] 69 | )->join( 70 | ['s' => 'icinga_services'], 71 | 's.service_object_id = so.object_id', 72 | [] 73 | )->where('so.is_active = 1') 74 | ->where('so.objecttype_id = 2') 75 | ->limit((int) $this->getValue('limit')) 76 | ->order('hostname ASC') 77 | ->order('servicename ASC'); 78 | 79 | if ($hostgroup) { 80 | $query->join( 81 | ['hgm' => 'icinga_hostgroup_members'], 82 | 's.host_object_id = hgm.host_object_id', 83 | [] 84 | )->join( 85 | ['hg' => 'icinga_hostgroups'], 86 | 'hg.hostgroup_id = hgm.hostgroup_id', 87 | [] 88 | )->join( 89 | ['hgo' => 'icinga_objects'], 90 | 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1', 91 | [] 92 | )->where('hgo.name1 = ?', $hostgroup); 93 | } 94 | 95 | if ($servicegroup) { 96 | $query->join( 97 | ['sgm' => 'icinga_servicegroup_members'], 98 | 's.service_object_id = sgm.service_object_id', 99 | [] 100 | )->join( 101 | ['sg' => 'icinga_servicegroups'], 102 | 'sg.servicegroup_id = sgm.servicegroup_id', 103 | [] 104 | )->join( 105 | ['sgo' => 'icinga_objects'], 106 | 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1', 107 | [] 108 | )->where('sgo.name1 = ?', $servicegroup); 109 | } 110 | 111 | $columns = [ 112 | 'hostname' => 'so.name1', 113 | 'servicename' => 'so.name2', 114 | ]; 115 | $this->addSlaColumnsToQuery($query, 'so.object_id', $columns); 116 | 117 | return $query; 118 | } 119 | 120 | protected function getMainCsvHeaders() 121 | { 122 | return ['Host', 'Service']; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /library/Reporting/Report/SlaReport.php: -------------------------------------------------------------------------------- 1 | $this->getResult(), 24 | 'timeframes' => $this->getSelectedTimeframes() 25 | ); 26 | } 27 | 28 | abstract protected function getMainCsvHeaders(); 29 | 30 | public function getCsv() 31 | { 32 | $filename = sprintf( 33 | '%s %s.csv', 34 | $this->getName(), 35 | date('(d.m.Y)') 36 | ); 37 | 38 | $headers = $this->getMainCsvHeaders(); 39 | foreach ($this->getSelectedTimeframes() as $timeFrame) { 40 | $headers[] = $timeFrame->getTitle(); 41 | } 42 | $rows = array($headers); 43 | foreach ($this->getResult() as $row) { 44 | $props = (array) $row; 45 | foreach ($props as $key => & $value) { 46 | if ($key === 'hostname' || $key === 'servicename') { 47 | continue; 48 | } 49 | 50 | $value = (float) $value; 51 | } 52 | $rows[] = array_values((array) $props); 53 | } 54 | return Csv::create($rows)->setFilename($filename); 55 | } 56 | 57 | protected function slaFunction($objectColumn, $timeframe) 58 | { 59 | return sprintf( 60 | "icinga_availability_slatime(%s, '%s', '%s', NULL)", 61 | //"icinga_availability2(%s, '%s', '%s')", 62 | $objectColumn, 63 | $timeframe->getStart(Timeframe::HUMAN), 64 | $timeframe->getEnd(Timeframe::HUMAN) 65 | ); 66 | } 67 | 68 | protected function getSelectedTimeframes() 69 | { 70 | return $this->configuredTimeframes()->get($this->getValue('timeframes')); 71 | } 72 | 73 | protected function addSlaColumnsToQuery($query, $objectColumn, $columns) 74 | { 75 | $slaColumns = $this->prepareSlaColumnsForTimeframes($objectColumn); 76 | $query->columns($columns + $slaColumns); 77 | reset($slaColumns); 78 | $query->order(key($slaColumns), 'ASC'); 79 | return $query; 80 | } 81 | 82 | protected function prepareSlaColumnsForTimeframes($objectColumn) 83 | { 84 | $columns = array(); 85 | $timeframes = $this->getSelectedTimeframes(); 86 | foreach ($timeframes as $alias => $timeframe) { 87 | $columns[$alias] = $this->slaFunction($objectColumn, $timeframe); 88 | } 89 | 90 | return $columns; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /library/Reporting/Timeframe.php: -------------------------------------------------------------------------------- 1 | name = $name; 30 | $this->title = $title; 31 | $this->start = $start; 32 | $this->end = $end; 33 | } 34 | 35 | public function getName() 36 | { 37 | return $this->name; 38 | } 39 | 40 | public function getTitle() 41 | { 42 | $title = $this->title; 43 | if (strpos($title, '{') !== false) { 44 | $start = $this->getStart(); 45 | $end = $this->getEnd(); 46 | $title = preg_replace('/{START_MONTHNAME}/', date('F', $start), $title); 47 | $title = preg_replace('/{START_MONTH}/', date('m', $start), $title); 48 | $title = preg_replace('/{START_YEAR}/', date('Y', $start), $title); 49 | $title = preg_replace('/{START_WEEK}/', date('W', $start), $title); 50 | $title = preg_replace('/{END_MONTHNAME}/', date('F', $end), $title); 51 | $title = preg_replace('/{END_MONTH}/', date('m', $end), $title); 52 | $title = preg_replace('/{END_YEAR}/', date('Y', $end), $title); 53 | $title = preg_replace('/{END_WEEK}/', date('W', $end), $title); 54 | } 55 | return $title; 56 | } 57 | 58 | public function getIntervalDescription() 59 | { 60 | return sprintf('%s - %s', $this->getStart(self::HUMAN), $this->getEnd(self::HUMAN)); 61 | } 62 | 63 | public function getStart($format = self::UNIX) 64 | { 65 | return $this->format($this->start, $format); 66 | } 67 | 68 | public function getEnd($format = self::UNIX) 69 | { 70 | return $this->format($this->end, $format); 71 | } 72 | 73 | public function toFilter($column = 'timestamp', $format = self::UNIX) 74 | { 75 | return Filter::matchAll( 76 | Filter::expression($column, '>=', $this->getStart($format)), 77 | Filter::expression($column, '<=', $this->getEnd($format)) 78 | ); 79 | } 80 | 81 | protected function format($time, $format) 82 | { 83 | if ($format === self::RAW) { 84 | return $time; 85 | } elseif ($format === self::UNIX) { 86 | return strtotime($time); 87 | } elseif ($format === self::HUMAN) { 88 | return date('Y-m-d H:i:s', strtotime($time)); 89 | } else { 90 | throw new NotImplementedError('No known time format has been requested'); 91 | } 92 | } 93 | 94 | public static function fromConfigSection($name, ConfigObject $config) 95 | { 96 | return new static( 97 | $name, 98 | $config->get('title', $name), 99 | $config->get('start'), 100 | $config->get('end') 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /library/Reporting/Timeframes.php: -------------------------------------------------------------------------------- 1 | timeframes as $key => $timeframe) { 21 | $enum[$key] = $timeframe->getTitle(); 22 | } 23 | 24 | return $enum; 25 | } 26 | 27 | public function get($name) 28 | { 29 | if (is_array($name)) { 30 | $timeframes = array(); 31 | foreach ($name as $key) { 32 | $timeframes[$key] = $this->get($key); 33 | } 34 | return $timeframes; 35 | } 36 | 37 | if (array_key_exists($name, $this->timeframes)) { 38 | return $this->timeframes[$name]; 39 | } 40 | 41 | throw new NotFoundError('No such timeframe defined: "%s"', $name); 42 | } 43 | 44 | public static function fromConfig(Config $config) 45 | { 46 | $self = new static; 47 | $self->timeframes = array(); 48 | 49 | foreach ($config as $name => $section) { 50 | $self->timeframes[$name] = Timeframe::fromConfigSection($name, $section); 51 | } 52 | 53 | return $self; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /library/Reporting/Web/Controller.php: -------------------------------------------------------------------------------- 1 | ido === null) { 16 | $this->ido = new Ido(); 17 | } 18 | 19 | return $this->ido; 20 | } 21 | 22 | public function loadForm($name) 23 | { 24 | return FormLoader::load($name, $this->Module()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /library/Reporting/Web/Form/CsrfToken.php: -------------------------------------------------------------------------------- 1 | getApplicationDir('forms'); 15 | $ns = '\\Icinga\\Web\\Forms\\'; 16 | } else { 17 | $basedir = $module->getFormDir(); 18 | $ns = '\\Icinga\\Module\\' . ucfirst($module->getName()) . '\\Forms\\'; 19 | } 20 | if (preg_match('~^[a-z0-9/]+$~i', $name)) { 21 | $parts = preg_split('~/~', $name); 22 | $class = ucfirst(array_pop($parts)) . 'Form'; 23 | $file = sprintf('%s/%s/%s.php', rtrim($basedir, '/'), implode('/', $parts), $class); 24 | if (file_exists($file)) { 25 | require_once($file); 26 | $class = $ns . $class; 27 | $options = array(); 28 | if ($module !== null) { 29 | $options['icingaModule'] = $module; 30 | } 31 | 32 | return new $class($options); 33 | } 34 | } 35 | throw new ProgrammingError(sprintf('Cannot load %s (%s), no such form', $name, $file)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /library/Reporting/Web/Form/QuickForm.php: -------------------------------------------------------------------------------- 1 | handleOptions($options)); 73 | $this->setMethod('post'); 74 | 75 | if (!Icinga::app()->isCli()) { 76 | $this->setAction(Url::fromRequest()); 77 | $this->createIdElement(); 78 | $this->regenerateCsrfToken(); 79 | } 80 | } 81 | 82 | protected function handleOptions($options = null) 83 | { 84 | if ($options === null) { 85 | return $options; 86 | } 87 | 88 | if (array_key_exists('icingaModule', $options)) { 89 | $this->icingaModule = $options['icingaModule']; 90 | $this->icingaModuleName = $this->icingaModule->getName(); 91 | unset($options['icingaModule']); 92 | } 93 | 94 | return $options; 95 | } 96 | 97 | protected function addSubmitButtonIfSet() 98 | { 99 | if (false !== ($label = $this->getSubmitLabel())) { 100 | $el = $this->createElement('submit', $label)->setLabel($label)->setDecorators(array('ViewHelper')); 101 | $this->submitButtonName = $el->getName(); 102 | $this->addElement($el); 103 | } 104 | } 105 | 106 | // TODO: This is ugly, we need to defer button creation 107 | protected function moveSubmitToBottom() 108 | { 109 | $name = $this->submitButtonName; 110 | if ($name && ($submit = $this->getElement($name))) { 111 | $this->removeElement($name); 112 | $this->addElement($submit); 113 | } 114 | } 115 | 116 | protected function createIdElement() 117 | { 118 | $this->detectName(); 119 | $this->addHidden(self::ID, $this->getName()); 120 | $this->getElement(self::ID)->setIgnore(true); 121 | } 122 | 123 | protected function getSentValue($name, $default = null) 124 | { 125 | $request = $this->getRequest(); 126 | 127 | if ($request->isPost()) { 128 | return $request->getPost($name); 129 | } else { 130 | return $default; 131 | } 132 | } 133 | 134 | public function getSubmitLabel() 135 | { 136 | if ($this->submitLabel === null) { 137 | return $this->translate('Submit'); 138 | } 139 | 140 | return $this->submitLabel; 141 | } 142 | 143 | public function setSubmitLabel($label) 144 | { 145 | $this->submitLabel = $label; 146 | return $this; 147 | } 148 | 149 | protected function loadForm($name, Module $module = null) 150 | { 151 | if ($module === null) { 152 | $module = $this->icingaModule; 153 | } 154 | 155 | return FormLoader::load($name, $module); 156 | } 157 | 158 | public function regenerateCsrfToken() 159 | { 160 | if (! $element = $this->getElement(self::CSRF)) { 161 | $this->addHidden(self::CSRF, CsrfToken::generate()); 162 | $element = $this->getElement(self::CSRF); 163 | } 164 | $element->setIgnore(true); 165 | 166 | return $this; 167 | } 168 | 169 | public function removeCsrfToken() 170 | { 171 | $this->removeElement(self::CSRF); 172 | return $this; 173 | } 174 | 175 | public function addHidden($name, $value = null) 176 | { 177 | $this->addElement('hidden', $name); 178 | $el = $this->getElement($name); 179 | $el->setDecorators(array('ViewHelper')); 180 | if ($value !== null) { 181 | $this->setDefault($name, $value); 182 | $el->setValue($value); 183 | } 184 | 185 | return $this; 186 | } 187 | 188 | public function addHtmlHint($html, $options = array()) 189 | { 190 | return $this->addHtml('
' . $html . '
', $options); 191 | } 192 | 193 | public function addHtml($html, $options = array()) 194 | { 195 | $name = '_HINT' . ++$this->hintCount; 196 | $this->addElement('note', $name, $options); 197 | $this->getElement($name) 198 | ->setValue($html) 199 | ->setIgnore(true) 200 | ->removeDecorator('Label'); 201 | 202 | return $this; 203 | } 204 | 205 | public function optionalEnum($enum) 206 | { 207 | return array( 208 | null => $this->translate('- please choose -') 209 | ) + $enum; 210 | } 211 | 212 | public function setSuccessUrl($url) 213 | { 214 | $this->successUrl = $url; 215 | return $this; 216 | } 217 | 218 | public function setup() 219 | { 220 | } 221 | 222 | protected function onSetup() 223 | { 224 | } 225 | 226 | public function setAction($action) 227 | { 228 | if (! $action instanceof Url) { 229 | $action = Url::fromPath($action); 230 | } 231 | return parent::setAction((string) $action); 232 | } 233 | 234 | public function setIcingaModule(Module $module) 235 | { 236 | $this->icingaModule = $module; 237 | return $this; 238 | } 239 | 240 | public function hasBeenSubmitted() 241 | { 242 | if ($this->hasBeenSubmitted === null) { 243 | $req = $this->getRequest(); 244 | if ($req->isPost()) { 245 | $post = $req->getPost(); 246 | $name = $this->submitButtonName; 247 | 248 | if ($name === null) { 249 | $this->hasBeenSubmitted = $this->hasBeenSent(); 250 | } else { 251 | $el = $this->getElement($name); 252 | $this->hasBeenSubmitted = array_key_exists($name, $post) 253 | && $post[$name] === $this->getSubmitLabel(); 254 | } 255 | } else { 256 | $this->hasBeenSubmitted === false; 257 | } 258 | } 259 | 260 | return $this->hasBeenSubmitted; 261 | } 262 | 263 | protected function beforeValidation($data = array()) 264 | { 265 | } 266 | 267 | public function prepareElements() 268 | { 269 | if (! $this->didSetup) { 270 | $this->setup(); 271 | $this->addSubmitButtonIfSet(); 272 | $this->onSetup(); 273 | $this->didSetup = true; 274 | } 275 | 276 | return $this; 277 | } 278 | 279 | public function handleValues($values = array()) 280 | { 281 | $_SERVER['REQUEST_METHOD'] = 'POST'; 282 | $request = new Request(); 283 | $this->hasBeenSent = true; 284 | $request->setPost($values); 285 | 286 | return $this->handleRequest($request); 287 | } 288 | 289 | public function handleRequest(Request $request = null) 290 | { 291 | if ($request !== null) { 292 | $this->setRequest($request); 293 | } 294 | 295 | if ($this->hasBeenSent()) { 296 | $post = $this->getRequest()->getPost(); 297 | if ($this->hasBeenSubmitted()) { 298 | $this->beforeValidation($post); 299 | if ($this->isValid($post)) { 300 | $this->onSuccess(); 301 | } else { 302 | $this->onFailure(); 303 | } 304 | } else { 305 | $this->setDefaults($post); 306 | } 307 | } else { 308 | // Well... 309 | } 310 | 311 | return $this; 312 | } 313 | 314 | public function translate($string) 315 | { 316 | if ($this->icingaModuleName === null) { 317 | return t($string); 318 | } else { 319 | return mt($this->icingaModuleName, $string); 320 | } 321 | } 322 | 323 | public function onSuccess() 324 | { 325 | $this->redirectOnSuccess(); 326 | } 327 | 328 | public function setSuccessMessage($message) 329 | { 330 | $this->successMessage = $message; 331 | return $this; 332 | } 333 | 334 | public function getSuccessMessage($message = null) 335 | { 336 | if ($message !== null) { 337 | return $message; 338 | } 339 | if ($this->successMessage === null) { 340 | return t('Form has successfully been sent'); 341 | } 342 | return $this->successMessage; 343 | } 344 | 345 | public function redirectOnSuccess($message = null) 346 | { 347 | if (Icinga::app()->isCli()) { 348 | return; 349 | } 350 | 351 | $url = $this->successUrl ?: $this->getAction(); 352 | $this->notifySuccess($this->getSuccessMessage($message)); 353 | $this->redirectAndExit($url); 354 | } 355 | 356 | public function onFailure() 357 | { 358 | } 359 | 360 | public function notifySuccess($message = null) 361 | { 362 | if ($message === null) { 363 | $message = t('Form has successfully been sent'); 364 | } 365 | Notification::success($message); 366 | return $this; 367 | } 368 | 369 | public function notifyError($message) 370 | { 371 | Notification::error($message); 372 | return $this; 373 | } 374 | 375 | protected function redirectAndExit($url) 376 | { 377 | Icinga::app()->getFrontController()->getResponse()->redirectAndExit($url); 378 | } 379 | 380 | protected function onRequest() 381 | { 382 | } 383 | 384 | public function setRequest(Request $request) 385 | { 386 | if ($this->request !== null) { 387 | throw new ProgrammingError('Unable to set request twice'); 388 | } 389 | 390 | $this->request = $request; 391 | $this->prepareElements(); 392 | $this->onRequest(); 393 | return $this; 394 | } 395 | 396 | public function getRequest() 397 | { 398 | if ($this->request === null) { 399 | $this->setRequest(Icinga::app()->getFrontController()->getRequest()); 400 | } 401 | return $this->request; 402 | } 403 | 404 | public function hasBeenSent() 405 | { 406 | if ($this->hasBeenSent === null) { 407 | $req = $this->getRequest(); 408 | if ($req->isPost()) { 409 | $post = $req->getPost(); 410 | $this->hasBeenSent = array_key_exists(self::ID, $post) && 411 | $post[self::ID] === $this->getName(); 412 | } else { 413 | $this->hasBeenSent === false; 414 | } 415 | } 416 | 417 | return $this->hasBeenSent; 418 | } 419 | 420 | protected function detectName() 421 | { 422 | if ($this->formName !== null) { 423 | $this->setName($this->formName); 424 | } else { 425 | $this->setName(get_class($this)); 426 | } 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /library/Reporting/Web/Hook/ReportHook.php: -------------------------------------------------------------------------------- 1 | splitClass(); 22 | $class = preg_replace('/Report/', '', array_pop($parts)); 23 | if (($module = $this->getModuleName()) !== 'Reporting') { 24 | return sprintf('%s (%s)', $class, $module); 25 | } 26 | 27 | return $class; 28 | } 29 | 30 | public function providesCsv() 31 | { 32 | return method_exists($this, 'getCsv'); 33 | } 34 | 35 | public function getValue($key, $default = null) 36 | { 37 | if (array_key_exists($key, $this->values)) { 38 | return $this->values[$key]; 39 | } 40 | 41 | return $default; 42 | } 43 | 44 | protected function getModuleName() 45 | { 46 | $parts = $this->splitClass(); 47 | if (array_shift($parts) === 'Icinga' && array_shift($parts) === 'Module') { 48 | return strtolower(array_shift($parts)); 49 | } 50 | 51 | return null; 52 | } 53 | 54 | protected function splitClass() 55 | { 56 | return explode('\\', get_class($this)); 57 | } 58 | 59 | protected function addTimeframesElement(QuickForm $form) 60 | { 61 | $form->addElement('multiselect', 'timeframes', array( 62 | 'label' => $form->translate('Timeframe(s)'), 63 | 'size' => 10, 64 | 'multiOptions' => $this->enumTimeframes(), 65 | 'required' => true, 66 | )); 67 | } 68 | 69 | protected function addTimeframeElement(QuickForm $form) 70 | { 71 | $form->addElement('select', 'timeframe', array( 72 | 'label' => $form->translate('Timeframe'), 73 | 'multiOptions' => $this->enumTimeframes(), 74 | 'required' => true, 75 | 'class' => 'autosubmit', 76 | )); 77 | } 78 | 79 | /** 80 | * @param $values 81 | * @return $this 82 | */ 83 | public function setValues($values) 84 | { 85 | $this->values = $values; 86 | return $this; 87 | } 88 | 89 | public function addFormElements(QuickForm $form) 90 | { 91 | } 92 | 93 | public function render($view) 94 | { 95 | $data = $this->getViewData(); 96 | $data['form'] = $view->reportForm; 97 | if (Icinga::app()->isCli()) { 98 | // CLI workaround 99 | return $view->partial( 100 | $this->getViewScript(), 101 | null, 102 | $data 103 | ); 104 | } else { 105 | return $view->partial( 106 | $this->getViewScript(), 107 | $this->getModuleName(), 108 | $data 109 | ); 110 | } 111 | } 112 | 113 | protected function enumTimeframes() 114 | { 115 | return $this->configuredTimeframes()->enumTimeframes(); 116 | } 117 | 118 | /** 119 | * @return Timeframes 120 | */ 121 | protected function configuredTimeframes() 122 | { 123 | return Timeframes::fromConfig( 124 | Config::module('reporting', 'timeframes') 125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /module.info: -------------------------------------------------------------------------------- 1 | Module: reporting 2 | Version: 0.0.0 3 | Description: Reporting module 4 | Provides some default reports and provides hooks for additional ones 5 | 6 | -------------------------------------------------------------------------------- /public/css/module.less: -------------------------------------------------------------------------------- 1 | div.reportFilter { 2 | width: 20em; 3 | float: left; 4 | vertical-align: top; 5 | 6 | dd { 7 | margin: 0; 8 | } 9 | } 10 | 11 | div.reportContent { 12 | vertical-align: top; 13 | margin-left: 20.5em; 14 | .actions { 15 | display: block; 16 | text-align: right; 17 | } 18 | } 19 | 20 | table.sla { 21 | width: 100%; 22 | font-size: 0.8em; 23 | border-collapse: separate; 24 | border-spacing: 0 1px; 25 | 26 | caption { 27 | caption-side: bottom; 28 | text-align: right; 29 | font-style: italic; 30 | } 31 | 32 | a { 33 | color: inherit; 34 | text-decoration: none; 35 | } 36 | 37 | a:hover { 38 | text-decoration: underline; 39 | } 40 | 41 | thead { 42 | th { 43 | text-align: center; 44 | border-bottom: 3px solid #666; 45 | padding: 0.3em 0.5em; 46 | } 47 | 48 | th:first-child { 49 | text-align: left; 50 | border-left: 1em solid #666; 51 | } 52 | } 53 | 54 | tbody { 55 | tr { 56 | padding: 0; 57 | margin: 0; 58 | } 59 | 60 | tr:nth-child(even) { 61 | background-color: #e6e6e6; 62 | } 63 | 64 | th { 65 | text-align: left; 66 | margin: 0; 67 | padding: 0; 68 | } 69 | 70 | td { 71 | width: 5em; 72 | text-align: right; 73 | padding: 0.1em; 74 | margin: 0; 75 | color: white; 76 | 77 | a { 78 | font-size: 0.85em; 79 | padding: 0.3em 0.5em; 80 | text-decoration: none; 81 | display: block; 82 | } 83 | } 84 | 85 | td:first-child { 86 | border-right: 1px solid #666; 87 | } 88 | 89 | td.ok { 90 | background-color: @colorOk; 91 | } 92 | 93 | td.nok { 94 | background-color: @colorCritical; 95 | } 96 | } 97 | } 98 | 99 | 100 | #layout.twocols { 101 | div.reportFilter { 102 | display: none; 103 | } 104 | div.reportContent { 105 | margin-left: 0; 106 | } 107 | table.sla { 108 | font-size: 0.7em; 109 | } 110 | } 111 | 112 | -------------------------------------------------------------------------------- /run.php: -------------------------------------------------------------------------------- 1 | provideHook( 4 | 'reporting/Report', 5 | '\\Icinga\\Module\\Reporting\\Report\\HostslaReport' 6 | ); 7 | 8 | $this->provideHook( 9 | 'reporting/Report', 10 | '\\Icinga\\Module\\Reporting\\Report\\ServiceslaReport' 11 | ); 12 | 13 | $this->provideHook('monitoring/dataviewExtension'); 14 | $this->provideHook('monitoring/idoQueryExtension'); 15 | 16 | -------------------------------------------------------------------------------- /schema/README.slaperiod: -------------------------------------------------------------------------------- 1 | Files 2 | ===== 3 | * slaperiod-schema.sql: creates time period tables 4 | * refresh_slaperiods-procedure.sql: procedure prefilling those tables 5 | * get_sladetail-procedure.sql: procedure able to retrieve SLA details for a single object 6 | 7 | Created tables 8 | -------------- 9 | * icinga_sla_periods: daily start/end events for each time period 10 | * icinga_outofsla_periods: inverted sla_periods (events differ!) 11 | 12 | Refreshing tables 13 | ----------------- 14 | The procedure is called this way: 15 | 16 | CALL icinga_refresh_slaperiods(); 17 | 18 | It should be called after each change to timeperiod objects, ideally by IDO2DB once all config objects have been dumped. 19 | 20 | 21 | Fetching SLA details 22 | -------------------- 23 | This new procedure cannot be part of a SELECT query, there has to be a dedicated CALL for each desired object. For a reporting suite as Jasper this implies running subreports for each single object. 24 | 25 | The procedure asks for four parameters: 26 | 27 | * object_id BIGINT UNSIGNED: the monitored object you are interested in 28 | * t_start DATETIME: interval start 29 | * t_end DATETIME: interval end 30 | * timeperiod_object_id BIGINT UNSIGNED: the SLA timeperiod object id 31 | 32 | A successful CALL may look as follows: 33 | 34 | mysql> CALL icinga_get_sladetail( 35 | 15373, 36 | '2013-08-01 00:00:00', 37 | '2013-08-31 23:59:59', 38 | 67192)\G 39 | *************************** 1. row *************************** 40 | sla_state0: 0.5875898251156755 41 | sla_state1: 0 42 | sla_state2: 0.3911780881041249 43 | sla_state3: 0.021232086780199663 44 | state0: 0.4925580542704802 45 | state1: 0 46 | state2: 0.4829974921585619 47 | state3: 0.024444453570957876 48 | 1 row in set (0.16 sec) 49 | 50 | Query OK, 0 rows affected (0.16 sec) 51 | 52 | You see a service with 58.75% availability if you consider the given SLA time period, but just 49.25% of "real" 24x7 availability. 53 | 54 | 55 | -------------------------------------------------------------------------------- /schema/get_sladetail-procedure.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP PROCEDURE IF EXISTS icinga_get_sladetail; 3 | 4 | DELIMITER $$ 5 | 6 | CREATE PROCEDURE icinga_get_sladetail( 7 | IN obj_id BIGINT UNSIGNED, 8 | IN t_start DATETIME, 9 | IN t_end DATETIME, 10 | IN tp_object_id BIGINT UNSIGNED 11 | ) 12 | 13 | READS SQL DATA 14 | 15 | BEGIN 16 | 17 | SET 18 | @last_state := NULL, 19 | @last_ts := NULL, 20 | @cnt_dt := NULL, 21 | @cnt_tp := NULL, 22 | @add_duration := NULL, 23 | @former_id := @id, 24 | @former_start := @start, 25 | @former_end := @end, 26 | @former_tp_id := @tp_object_id, 27 | @id := obj_id, 28 | @start := t_start, 29 | @end := t_end, 30 | @tp_object_id := tp_object_id 31 | ; 32 | 33 | SELECT 34 | SUM(CASE WHEN current_state = 0 THEN duration ELSE 0 END) / (UNIX_TIMESTAMP(@end) - UNIX_TIMESTAMP(@start)) AS sla_state0, 35 | SUM(CASE WHEN current_state = 1 THEN duration ELSE 0 END) / (UNIX_TIMESTAMP(@end) - UNIX_TIMESTAMP(@start)) AS sla_state1, 36 | SUM(CASE WHEN current_state = 2 THEN duration ELSE 0 END) / (UNIX_TIMESTAMP(@end) - UNIX_TIMESTAMP(@start)) AS sla_state2, 37 | SUM(CASE WHEN current_state = 3 THEN duration ELSE 0 END) / (UNIX_TIMESTAMP(@end) - UNIX_TIMESTAMP(@start)) AS sla_state3, 38 | SUM(CASE WHEN real_state = 0 THEN duration ELSE 0 END) / (UNIX_TIMESTAMP(@end) - UNIX_TIMESTAMP(@start)) AS state0, 39 | SUM(CASE WHEN real_state = 1 THEN duration ELSE 0 END) / (UNIX_TIMESTAMP(@end) - UNIX_TIMESTAMP(@start)) AS state1, 40 | SUM(CASE WHEN real_state = 2 THEN duration ELSE 0 END) / (UNIX_TIMESTAMP(@end) - UNIX_TIMESTAMP(@start)) AS state2, 41 | SUM(CASE WHEN real_state = 3 THEN duration ELSE 0 END) / (UNIX_TIMESTAMP(@end) - UNIX_TIMESTAMP(@start)) AS state3 42 | 43 | FROM ( SELECT 44 | 45 | CASE WHEN @last_ts IS NULL THEN 46 | -- ...remember the duration and return 0... 47 | (@add_duration := COALESCE(@add_duration, 0) 48 | + UNIX_TIMESTAMP(state_time) 49 | - UNIX_TIMESTAMP(COALESCE(@last_ts, @start)) + 1 50 | ) - 1 51 | ELSE 52 | -- ...otherwise return a correct duration... 53 | UNIX_TIMESTAMP(state_time) 54 | - UNIX_TIMESTAMP(COALESCE(@last_ts, @start)) 55 | -- ...and don't forget to add what we remembered 'til now: 56 | + COALESCE(CASE @cnt_dt + @cnt_tp WHEN 0 THEN @add_duration ELSE NULL END, 0) 57 | END AS duration, 58 | 59 | -- current_state is the state from the last state change until now, this one is the one the timeperiod is for: 60 | CASE WHEN COALESCE(@cnt_dt, 0) + COALESCE(@cnt_tp, 0) >= 1 THEN 0 ELSE COALESCE(@last_state, last_state) END AS current_state, 61 | 62 | -- Workaround for a nasty Icinga issue. In case a hard state is reached 63 | -- before max_check_attempts, the last_hard_state value is wrong. As of 64 | -- this we are stepping through all single events, even soft ones. 65 | -- See https://dev.icinga.org/issues/4734 66 | -- Of course soft states do not have an influence on the availability: 67 | 68 | -- real_state is the current state regardless of whether we are in a downtime or not 69 | COALESCE(@last_state, last_state) AS real_state, 70 | 71 | -- next_state is the state from now on, so it replaces @last_state: 72 | CASE 73 | -- Set our next @last_state if we have a hard state change 74 | WHEN type IN ('hard_state', 'former_state', 'current_state') THEN @last_state := state 75 | -- ...or if there is a soft_state and no @last_state has been seen before 76 | WHEN type = 'soft_state' THEN 77 | -- If we don't have a @last_state... 78 | CASE WHEN @last_state IS NULL 79 | -- ...use and set our own last_hard_state (last_state is an alias here)... 80 | THEN @last_state := last_state 81 | -- ...and return @last_state otherwise, as soft states shall have no 82 | -- impact on availability 83 | ELSE @last_state END 84 | 85 | WHEN type IN ('dt_start', 'sla_end') THEN 0 86 | WHEN type IN ('dt_end', 'sla_start') THEN @last_state 87 | END AS next_state, 88 | 89 | -- Then set @add_duration to NULL in case we got a new @last_ts 90 | COALESCE( 91 | CASE WHEN @add_duration IS NOT NULL AND @cnt_dt = 0 AND @cnt_tp = 0 92 | THEN @add_duration 93 | ELSE @add_duration := null 94 | END, 95 | 0 96 | ) AS addd, 97 | 98 | -- First raise or lower our downtime counter 99 | -- TODO: Distinct counters for sla and dt, they are not related to each other 100 | CASE type 101 | WHEN 'dt_start' THEN @cnt_dt := COALESCE(@cnt_dt, 0) + 1 102 | WHEN 'dt_end' THEN @cnt_dt := GREATEST(@cnt_dt - 1, 0) 103 | ELSE COALESCE(@cnt_dt, 0) 104 | END AS dt_depth, 105 | 106 | CASE type 107 | WHEN 'sla_end' THEN @cnt_tp := COALESCE(@cnt_tp, 0) + 1 108 | WHEN 'sla_start' THEN @cnt_tp := GREATEST(@cnt_tp - 1, 0) 109 | ELSE COALESCE(@cnt_tp, 0) 110 | END AS tp_depth, 111 | 112 | -- Also fetch the event type 113 | type, 114 | 115 | -- Our start_time is either the last end_time or @start... 116 | COALESCE(@last_ts, @start) AS start_time, 117 | 118 | -- ...end when setting the new end_time we remember it in @last_ts: 119 | @last_ts := state_time AS end_time 120 | 121 | FROM ( 122 | 123 | -- START fetching statehistory events 124 | SELECT 125 | state_time, 126 | CASE state_type WHEN 1 THEN 'hard_state' ELSE 'soft_state' END AS type, 127 | state, 128 | CASE state_type WHEN 1 THEN last_state ELSE last_hard_state END AS last_state 129 | FROM icinga_statehistory 130 | WHERE object_id = @id 131 | AND state_time >= @start 132 | AND state_time <= @end 133 | -- STOP fetching statehistory events 134 | 135 | -- START fetching current host state as an event 136 | -- TODO: This is not 100% correct. state should be find, last_state sometimes isn't. 137 | UNION SELECT 138 | GREATEST( 139 | @start, 140 | CASE state_type WHEN 1 THEN last_state_change ELSE last_hard_state_change END 141 | ) AS state_time, 142 | 'current_state' AS type, 143 | CASE state_type WHEN 1 THEN current_state ELSE last_hard_state END AS state, 144 | last_hard_state AS last_state 145 | FROM icinga_hoststatus 146 | WHERE CASE state_type WHEN 1 THEN last_state_change ELSE last_hard_state_change END < @start 147 | AND host_object_id = @id 148 | AND CASE state_type WHEN 1 THEN last_state_change ELSE last_hard_state_change END <= @end 149 | AND status_update_time > @start 150 | -- END fetching current host state as an event 151 | 152 | -- START fetching current service state as an event 153 | UNION SELECT 154 | GREATEST( 155 | @start, 156 | CASE state_type WHEN 1 THEN last_state_change ELSE last_hard_state_change END 157 | ) AS state_time, 158 | 'current_state' AS type, 159 | CASE state_type WHEN 1 THEN current_state ELSE last_hard_state END AS state, 160 | last_hard_state AS last_state 161 | FROM icinga_servicestatus 162 | WHERE CASE state_type WHEN 1 THEN last_state_change ELSE last_hard_state_change END < @start 163 | AND service_object_id = @id 164 | AND CASE state_type WHEN 1 THEN last_state_change ELSE last_hard_state_change END <= @end 165 | AND status_update_time > @start 166 | -- END fetching current service state as an event 167 | 168 | -- START fetching last state BEFORE the given interval as an event 169 | UNION ALL SELECT * FROM ( 170 | SELECT 171 | @start AS state_time, 172 | 'former_state' AS type, 173 | 0 AS state, 174 | 1 AS last_state 175 | ) formerstate 176 | -- END fetching last state BEFORE the given interval as an event 177 | 178 | -- START fetching first state AFTER the given interval as an event 179 | UNION ALL SELECT * FROM ( 180 | SELECT 181 | @end AS state_time, 182 | 'future_state' AS type, 183 | 1 AS state, 184 | 0 AS last_state 185 | ) futurestate 186 | -- END fetching first state AFTER the given interval as an event 187 | 188 | -- START ADDING a fake end 189 | UNION ALL SELECT 190 | @end AS state_time, 191 | 'dt_start' AS type, 192 | NULL AS state, 193 | NULL AS last_state 194 | FROM DUAL 195 | -- END ADDING a fake end 196 | 197 | -- START adding add all related downtime start times 198 | -- TODO: Handling downtimes still being active would be nice. 199 | -- But pay attention: they could be completely outdated 200 | UNION ALL SELECT 201 | GREATEST(actual_start_time, @start) AS state_time, 202 | 'dt_start' AS type, 203 | NULL AS state, 204 | NULL AS last_state 205 | FROM icinga_downtimehistory 206 | WHERE object_id = @id 207 | AND actual_start_time < @end 208 | AND actual_end_time > @start 209 | -- STOP adding add all related downtime start times 210 | 211 | -- START adding add all related downtime end times 212 | UNION ALL SELECT 213 | LEAST(actual_end_time, @end) AS state_time, 214 | 'dt_end' AS type, 215 | NULL AS state, 216 | NULL AS last_state 217 | FROM icinga_downtimehistory 218 | WHERE object_id = @id 219 | AND actual_start_time < @end 220 | AND actual_end_time > @start 221 | -- STOP adding add all related downtime end times 222 | 223 | -- START fetching SLA time period start times --- 224 | -- TODO: * reset @tp_*?? 225 | -- * find better sub table aliases 226 | UNION ALL 227 | SELECT 228 | start_time AS state_time, 229 | 'sla_start' AS type, 230 | NULL AS state, 231 | NULL AS last_state 232 | FROM icinga_outofsla_periods 233 | WHERE timeperiod_object_id = @tp_object_id 234 | AND start_time >= @start AND start_time <= @end 235 | 236 | -- STOP fetching SLA time period start times --- 237 | 238 | -- START fetching SLA time period end times --- 239 | UNION ALL SELECT 240 | end_time AS state_time, 241 | 'sla_start' AS type, 242 | NULL AS state, 243 | NULL AS last_state 244 | FROM icinga_outofsla_periods 245 | WHERE timeperiod_object_id = @tp_object_id 246 | AND end_time >= @start AND end_time <= @end 247 | -- STOP fetching SLA time period end times --- 248 | 249 | ) events 250 | ORDER BY events.state_time ASC, 251 | CASE events.type 252 | WHEN 'former_state' THEN 0 253 | WHEN 'soft_state' THEN 1 254 | WHEN 'hard_state' THEN 2 255 | WHEN 'current_state' THEN 3 256 | WHEN 'future_state' THEN 4 257 | WHEN 'sla_end' THEN 5 258 | WHEN 'sla_start' THEN 6 259 | WHEN 'dt_start' THEN 7 260 | WHEN 'dt_end' THEN 8 261 | ELSE 9 262 | END ASC 263 | ) events_with_bla; 264 | 265 | SET 266 | @last_state := NULL, 267 | @last_ts := NULL, 268 | @cnt_dt := NULL, 269 | @cnt_tp := NULL, 270 | @add_duration := NULL, 271 | @id := @former_id, 272 | @start := @former_start, 273 | @end := @former_end, 274 | @tp_object_id := @former_tp_id 275 | ; 276 | 277 | END; 278 | $$ 279 | DELIMITER ; 280 | -------------------------------------------------------------------------------- /schema/icinga_sla.sql: -------------------------------------------------------------------------------- 1 | -- --------------------------------------------------- -- 2 | -- SLA function for Icinga/IDO -- 3 | -- -- 4 | -- Author : Icinga Developer Team -- 5 | -- Copyright : 2012 Icinga Developer Team -- 6 | -- License : GPL 2.0 -- 7 | -- --------------------------------------------------- -- 8 | 9 | -- 10 | -- History 11 | -- 12 | -- 2012-08-31: Added to Icinga 13 | -- 2013-08-20: Simplified and improved 14 | -- 2013-08-23: Refactored, added SLA time period support 15 | -- 16 | 17 | DELIMITER | 18 | 19 | DROP FUNCTION IF EXISTS icinga_availability_slatime| 20 | CREATE FUNCTION icinga_availability_slatime ( 21 | id BIGINT UNSIGNED, 22 | start DATETIME, 23 | end DATETIME, 24 | sla_timeperiod_object_id BIGINT UNSIGNED 25 | ) RETURNS DECIMAL(7, 4) 26 | READS SQL DATA 27 | BEGIN 28 | DECLARE availability DECIMAL(7, 4); 29 | DECLARE dummy_id BIGINT UNSIGNED; 30 | 31 | SELECT @type_id := objecttype_id INTO dummy_id FROM icinga_objects WHERE object_id = id; 32 | 33 | IF @type_id NOT IN (1, 2) THEN 34 | RETURN NULL; 35 | END IF; 36 | 37 | -- We'll use @-Vars, this allows easy testing of subqueries without a function 38 | SET @former_id := @id, 39 | @tp_lastday := -1, 40 | @tp_lastend := 0, 41 | @former_sla_timeperiod_object_id := @sla_timeperiod_object_id, 42 | @former_start := @start, 43 | @former_end := @end, 44 | @sla_timeperiod_object_id := sla_timeperiod_object_id, 45 | @id := id, 46 | @start := start, 47 | @end := end, 48 | @day_offset := -1, 49 | @last_state := NULL, 50 | @last_ts := NULL, 51 | @cnt_dt := NULL, 52 | @cnt_tp := NULL, 53 | @add_duration := NULL; 54 | 55 | 56 | SELECT 57 | CAST(SUM( 58 | (duration) -- why?? 59 | / 60 | -- ... divided through the chosen time period duration... 61 | (UNIX_TIMESTAMP(@end) - UNIX_TIMESTAMP(@start)) 62 | -- ...multiplying the result with 100 (%)... 63 | * 100 64 | -- ...ignoring all but OK, WARN or UP states... 65 | * IF (@type_id = 1, IF(current_state = 0, 1, 0), IF (current_state < 2, 1, 0)) 66 | ) AS DECIMAL(7, 4)) 67 | 68 | -- START fetching the whole normalized eventhistory with durations 69 | INTO availability FROM ( SELECT 70 | 71 | CASE WHEN @last_ts IS NULL THEN 72 | -- ...remember the duration and return 0... 73 | (@add_duration := COALESCE(@add_duration, 0) 74 | + UNIX_TIMESTAMP(state_time) 75 | - UNIX_TIMESTAMP(COALESCE(@last_ts, @start)) + 1 76 | ) - 1 77 | ELSE 78 | -- ...otherwise return a correct duration... 79 | UNIX_TIMESTAMP(state_time) 80 | - UNIX_TIMESTAMP(COALESCE(@last_ts, @start)) 81 | -- ...and don't forget to add what we remembered 'til now: 82 | + COALESCE(CASE @cnt_dt + @cnt_tp WHEN 0 THEN @add_duration ELSE NULL END, 0) 83 | END AS duration, 84 | 85 | -- current_state is the state from the last state change until now: 86 | CASE WHEN @cnt_dt + @cnt_tp >= 1 THEN 0 ELSE COALESCE(@last_state, last_state) END AS current_state, 87 | 88 | -- next_state is the state from now on, so it replaces @last_state: 89 | CASE 90 | -- Set our next @last_state if we have a hard state change 91 | WHEN type IN ('hard_state', 'former_state', 'current_state') THEN @last_state := state 92 | -- ...or if there is a soft_state and no @last_state has been seen before 93 | WHEN type = 'soft_state' THEN 94 | -- If we don't have a @last_state... 95 | CASE WHEN @last_state IS NULL 96 | -- ...use and set our own last_hard_state (last_state is an alias here)... 97 | THEN @last_state := last_state 98 | -- ...and return @last_state otherwise, as soft states shall have no 99 | -- impact on availability 100 | ELSE @last_state END 101 | 102 | WHEN type IN ('dt_start', 'sla_end') THEN 0 103 | WHEN type IN ('dt_end', 'sla_start') THEN @last_state 104 | END AS next_state, 105 | 106 | -- Then set @add_duration to NULL in case we got a new @last_ts 107 | COALESCE( 108 | CASE WHEN @add_duration IS NOT NULL AND @cnt_dt = 0 AND @cnt_tp = 0 109 | THEN @add_duration 110 | ELSE @add_duration := null 111 | END, 112 | 0 113 | ) AS addd, 114 | 115 | -- First raise or lower our downtime counter 116 | -- TODO: Distinct counters for sla and dt, they are not related to each other 117 | CASE type 118 | WHEN 'dt_start' THEN @cnt_dt := COALESCE(@cnt_dt, 0) + 1 119 | WHEN 'dt_end' THEN @cnt_dt := GREATEST(@cnt_dt - 1, 0) 120 | WHEN 'sla_end' THEN @cnt_tp := COALESCE(@cnt_tp, 0) + 1 121 | WHEN 'sla_start' THEN @cnt_tp := GREATEST(@cnt_tp - 1, 0) 122 | ELSE @cnt_dt + @cnt_tp -- UGLY 123 | END AS dt_depth, 124 | 125 | -- Also fetch the event type 126 | type, 127 | 128 | -- Our start_time is either the last end_time or @start... 129 | COALESCE(@last_ts, @start) AS start_time, 130 | 131 | -- ...end when setting the new end_time we remember it in @last_ts: 132 | @last_ts := state_time AS end_time 133 | 134 | FROM ( 135 | 136 | -- START fetching statehistory events 137 | SELECT 138 | state_time, 139 | CASE state_type WHEN 1 THEN 'hard_state' ELSE 'soft_state' END AS type, 140 | state, 141 | -- Workaround for a nasty Icinga issue. In case a hard state is reached 142 | -- before max_check_attempts, the last_hard_state value is wrong. As of 143 | -- this we are stepping through all single events, even soft ones. Of 144 | -- course soft states do not have an influence on the availability: 145 | CASE state_type WHEN 1 THEN last_state ELSE last_hard_state END AS last_state 146 | FROM icinga_statehistory 147 | WHERE object_id = @id 148 | AND state_time >= @start 149 | AND state_time <= @end 150 | -- STOP fetching statehistory events 151 | 152 | -- START fetching last state BEFORE the given interval as an event 153 | UNION SELECT * FROM ( 154 | SELECT 155 | @start AS state_time, 156 | 'former_state' AS type, 157 | CASE state_type WHEN 1 THEN state ELSE last_hard_state END AS state, 158 | CASE state_type WHEN 1 THEN last_state ELSE last_hard_state END AS last_state 159 | FROM icinga_statehistory h 160 | WHERE object_id = @id 161 | AND state_time < @start 162 | ORDER BY h.state_time DESC LIMIT 1 163 | ) formerstate 164 | -- END fetching last state BEFORE the given interval as an event 165 | 166 | -- START fetching first state AFTER the given interval as an event 167 | UNION SELECT * FROM ( 168 | SELECT 169 | @end AS state_time, 170 | 'future_state' AS type, 171 | CASE state_type WHEN 1 THEN last_state ELSE last_hard_state END AS state, 172 | CASE state_type WHEN 1 THEN state ELSE last_hard_state END AS last_state 173 | FROM icinga_statehistory h 174 | WHERE object_id = @id 175 | AND state_time > @end 176 | ORDER BY h.state_time ASC LIMIT 1 177 | ) futurestate 178 | -- END fetching first state AFTER the given interval as an event 179 | 180 | -- START ADDING a fake end 181 | UNION SELECT 182 | @end AS state_time, 183 | 'dt_start' AS type, 184 | NULL AS state, 185 | NULL AS last_state 186 | FROM DUAL 187 | -- END ADDING a fake end 188 | 189 | -- START fetching current host state as an event 190 | -- TODO: This is not 100% correct. state should be fine, last_state sometimes isn't. 191 | UNION SELECT 192 | GREATEST( 193 | @start, 194 | CASE state_type WHEN 1 THEN last_state_change ELSE last_hard_state_change END 195 | ) AS state_time, 196 | 'current_state' AS type, 197 | CASE state_type WHEN 1 THEN current_state ELSE last_hard_state END AS state, 198 | last_hard_state AS last_state 199 | FROM icinga_hoststatus 200 | WHERE CASE state_type WHEN 1 THEN last_state_change ELSE last_hard_state_change END < @start 201 | AND host_object_id = @id 202 | AND CASE state_type WHEN 1 THEN last_state_change ELSE last_hard_state_change END <= @end 203 | AND status_update_time > @start 204 | -- END fetching current host state as an event 205 | 206 | -- START fetching current service state as an event 207 | UNION SELECT 208 | GREATEST( 209 | @start, 210 | CASE state_type WHEN 1 THEN last_state_change ELSE last_hard_state_change END 211 | ) AS state_time, 212 | 'current_state' AS type, 213 | CASE state_type WHEN 1 THEN current_state ELSE last_hard_state END AS state, 214 | last_hard_state AS last_state 215 | FROM icinga_servicestatus 216 | WHERE CASE state_type WHEN 1 THEN last_state_change ELSE last_hard_state_change END < @start 217 | AND service_object_id = @id 218 | AND CASE state_type WHEN 1 THEN last_state_change ELSE last_hard_state_change END <= @end 219 | AND status_update_time > @start 220 | -- END fetching current service state as an event 221 | 222 | -- START adding add all related downtime start times 223 | -- TODO: Handling downtimes still being active would be nice. 224 | -- But pay attention: they could be completely outdated 225 | UNION SELECT 226 | GREATEST(actual_start_time, @start) AS state_time, 227 | 'dt_start' AS type, 228 | NULL AS state, 229 | NULL AS last_state 230 | FROM icinga_downtimehistory 231 | WHERE object_id = @id 232 | AND actual_start_time < @end 233 | AND actual_end_time > @start 234 | -- STOP adding add all related downtime start times 235 | 236 | -- START adding add all related downtime end times 237 | UNION SELECT 238 | LEAST(actual_end_time, @end) AS state_time, 239 | 'dt_end' AS type, 240 | NULL AS state, 241 | NULL AS last_state 242 | FROM icinga_downtimehistory 243 | WHERE object_id = @id 244 | AND actual_start_time < @end 245 | AND actual_end_time > @start 246 | -- STOP adding add all related downtime end times 247 | 248 | -- START fetching SLA time period start times --- 249 | UNION ALL 250 | SELECT 251 | start_time AS state_time, 252 | 'sla_start' AS type, 253 | NULL AS state, 254 | NULL AS last_state 255 | FROM icinga_outofsla_periods 256 | WHERE timeperiod_object_id = @sla_timeperiod_object_id 257 | AND start_time >= @start AND start_time <= @end 258 | -- STOP fetching SLA time period start times --- 259 | 260 | -- START fetching SLA time period end times --- 261 | UNION ALL SELECT 262 | end_time AS state_time, 263 | 'sla_end' AS type, 264 | NULL AS state, 265 | NULL AS last_state 266 | FROM icinga_outofsla_periods 267 | WHERE timeperiod_object_id = @sla_timeperiod_object_id 268 | AND end_time >= @start AND end_time <= @end 269 | -- STOP fetching SLA time period end times --- 270 | 271 | ) events 272 | ORDER BY events.state_time ASC, 273 | CASE events.type 274 | WHEN 'former_state' THEN 0 275 | WHEN 'soft_state' THEN 1 276 | WHEN 'hard_state' THEN 2 277 | WHEN 'current_state' THEN 3 278 | WHEN 'future_state' THEN 4 279 | WHEN 'sla_end' THEN 5 280 | WHEN 'sla_start' THEN 6 281 | WHEN 'dt_start' THEN 7 282 | WHEN 'dt_end' THEN 8 283 | ELSE 9 284 | END ASC 285 | ) events_with_duration; 286 | -- END fetching the whole normalized eventhistory with durations 287 | 288 | -- Restore other vars 289 | SET @id := @former_id, 290 | @start := @former_start, 291 | @end := @former_end, 292 | @sla_timeperiod_object_id := @former_sla_timeperiod_object_id, 293 | @last_state := NULL, 294 | @last_ts := NULL, 295 | @cnt_dt := NULL, 296 | @cnt_tp := NULL, 297 | @add_duration := NULL, 298 | @type_id := NULL, 299 | @tp_lastday := NULL, 300 | @tp_lastend := NULL, 301 | @day_offset := NULL; 302 | 303 | RETURN availability; 304 | END| 305 | 306 | DELIMITER ; 307 | -------------------------------------------------------------------------------- /schema/refresh_outofslaperiods-procedure.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP PROCEDURE IF EXISTS icinga_refresh_outofslaperiods; 3 | 4 | DELIMITER $$ 5 | 6 | CREATE PROCEDURE icinga_refresh_outofslaperiods() 7 | SQL SECURITY INVOKER 8 | BEGIN 9 | DECLARE t_start DATETIME; 10 | DECLARE t_end DATETIME; 11 | DECLARE tp_id, tpo_id BIGINT UNSIGNED; 12 | DECLARE ts_offset INT; 13 | DECLARE fake_result INT UNSIGNED; 14 | 15 | DECLARE done INT DEFAULT FALSE; 16 | 17 | DECLARE cursor_tp CURSOR FOR SELECT 18 | tpo.object_id, 19 | tp.timeperiod_object_id 20 | FROM icinga_timeperiods tp 21 | JOIN icinga_objects tpo ON tp.timeperiod_object_id = tpo.object_id 22 | AND tpo.is_active = 1; 23 | 24 | DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; 25 | 26 | -- Workaround for WARNINGS complaining about unsafe statement (insert using Limit) 27 | SET SESSION binlog_format = ROW; 28 | 29 | START TRANSACTION; 30 | 31 | TRUNCATE TABLE icinga_outofsla_periods; 32 | 33 | SELECT 34 | CAST(DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 4 YEAR), '%Y-01-01 00:00:00') AS DATETIME), 35 | CAST(DATE_FORMAT(DATE_ADD(NOW(), INTERVAL 1 YEAR), '%Y-12-31 23:59:59') AS DATETIME), 36 | -- Icinga 2 writes seconds with timestamp offset into columns not aware of timezones 37 | -- This is an attempt to fix those values: 38 | CASE WHEN COALESCE( 39 | (SELECT CASE WHEN program_version LIKE 'v2%' THEN 1 ELSE 0 END 40 | FROM icinga_programstatus 41 | WHERE is_currently_running = 1 42 | ORDER BY status_update_time DESC 43 | ), 44 | 1 45 | ) = 1 THEN TIMESTAMPDIFF(SECOND, UTC_TIMESTAMP(), NOW()) ELSE 0 END 46 | INTO t_start, t_end, ts_offset; 47 | 48 | OPEN cursor_tp; 49 | 50 | tp_loop: LOOP 51 | FETCH cursor_tp INTO tp_id, tpo_id; 52 | IF done THEN 53 | LEAVE tp_loop; 54 | END IF; 55 | 56 | SET @tp_lastend := NULL, 57 | @tp_lastday := NULL, 58 | @day_offset := NULL; 59 | 60 | INSERT 61 | INTO icinga_outofsla_periods SELECT 62 | tpo_id, 63 | DATE_ADD(CAST(monthly.date AS DATETIME), INTERVAL finaltps.start_sec SECOND) AS start_time, 64 | DATE_ADD(CAST(monthly.date AS DATETIME), INTERVAL finaltps.end_sec SECOND) AS end_time 65 | 66 | FROM ( 67 | SELECT 68 | DATE_ADD(DATE(t_start), INTERVAL @day_offset := @day_offset + 1 DAY) AS date, 69 | DAYOFWEEK(DATE_ADD(DATE(t_start), INTERVAL @day_offset DAY)) - 1 AS weekday 70 | -- there is no generate_series or similar in MySQL, we need at least 2192 lines: 71 | FROM icinga_objects o 72 | JOIN (SELECT @day_offset := -1) day_offset 73 | -- 6 years (this one plus last 5 years): 74 | ORDER BY object_id 75 | LIMIT 2194 76 | ) monthly JOIN ( 77 | SELECT 78 | NULL AS day, 79 | NULL as start_sec, 80 | NULL AS end_sec 81 | FROM DUAL 82 | WHERE (@tp_lastday := NULL) IS NOT NULL 83 | AND ((@tp_lastend := 0) + (@day_offset := -1)) = 1 84 | 85 | UNION SELECT 86 | cleantps.day, 87 | cleantps.start_sec, 88 | cleantps.end_sec 89 | FROM ( 90 | SELECT 91 | @tp_lastday AS last_day, 92 | sortedtps.day AS day, 93 | CASE sortedtps.day WHEN @tp_lastday THEN @tp_lastend ELSE (@tp_lastday := sortedtps.day) - sortedtps.day END start_sec, 94 | sortedtps.start_sec + (@tp_lastend := sortedtps.end_sec) * 0 AS end_sec 95 | FROM ( 96 | SELECT 97 | singletps.day, 98 | singletps.start_sec, 99 | singletps.end_sec 100 | FROM ( 101 | -- fake start on every day 102 | SELECT * FROM ( 103 | SELECT 104 | @day_offset := CASE 105 | WHEN @day_offset < 6 AND @day_offset >= 0 AND @day_offset IS NOT NULL 106 | THEN @day_offset + 1 107 | ELSE 0 108 | END AS day, 109 | 0 AS start_sec, 110 | 0 AS end_sec 111 | -- Fake generate_series: 112 | FROM icinga_objects 113 | LIMIT 7 114 | ) fake_start 115 | UNION 116 | SELECT * FROM ( 117 | -- Fake end for every day 118 | SELECT 119 | @day_offset := CASE 120 | WHEN @day_offset < 6 AND @day_offset >= 0 AND @day_offset IS NOT NULL 121 | THEN @day_offset + 1 122 | ELSE 0 123 | END AS day, 124 | 86400 AS start_sec, 125 | 86400 AS end_sec 126 | -- Fake generate_series: 127 | FROM icinga_objects 128 | LIMIT 7 129 | ) fake_end 130 | UNION 131 | -- configured time ranges 132 | SELECT 133 | day, 134 | (start_sec + ts_offset) % 86400 AS start_sec, 135 | CASE WHEN (end_sec + ts_offset) = 86400 THEN 86400 ELSE (end_sec + ts_offset) % 86400 END AS end_sec 136 | FROM icinga_timeperiod_timeranges tpr 137 | 138 | JOIN icinga_timeperiods tp ON tp.timeperiod_id = tpr.timeperiod_id 139 | WHERE tp.timeperiod_object_id = tpo_id 140 | ) singletps 141 | ORDER BY singletps.day, singletps.start_sec, singletps.end_sec 142 | ) sortedtps 143 | ) cleantps 144 | WHERE cleantps.end_sec > cleantps.start_sec 145 | ) finaltps ON finaltps.day = monthly.weekday 146 | WHERE DATE_ADD(CAST(monthly.date AS DATETIME), INTERVAL finaltps.end_sec - 1 SECOND) <= t_end; 147 | 148 | END LOOP tp_loop; 149 | 150 | CLOSE cursor_tp; 151 | 152 | COMMIT; 153 | SET SESSION binlog_format = STATEMENT; 154 | 155 | -- Workaround for nasty warning: 156 | -- | Error | 1329 | No data - zero rows fetched, selected, or processed | 157 | SELECT 0 INTO fake_result FROM icinga_objects LIMIT 1; 158 | END; 159 | $$ 160 | DELIMITER ; 161 | -------------------------------------------------------------------------------- /schema/refresh_slaperiods-procedure.sql: -------------------------------------------------------------------------------- 1 | DROP PROCEDURE IF EXISTS icinga_refresh_slaperiods; 2 | 3 | DELIMITER $$ 4 | 5 | CREATE PROCEDURE icinga_refresh_slaperiods() 6 | SQL SECURITY INVOKER 7 | BEGIN 8 | DECLARE t_start DATETIME; 9 | DECLARE t_end DATETIME; 10 | DECLARE tp_id, tpo_id BIGINT UNSIGNED; 11 | DECLARE ts_offset INT; 12 | DECLARE fake_result INT UNSIGNED; 13 | 14 | DECLARE done INT DEFAULT FALSE; 15 | 16 | DECLARE cursor_tp CURSOR FOR SELECT 17 | tpo.object_id, 18 | tp.timeperiod_object_id 19 | FROM icinga_timeperiods tp 20 | JOIN icinga_objects tpo ON tp.timeperiod_object_id = tpo.object_id 21 | AND tpo.is_active = 1; 22 | 23 | DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; 24 | 25 | 26 | SET SESSION binlog_format = ROW; 27 | 28 | START TRANSACTION; 29 | 30 | TRUNCATE TABLE icinga_sla_periods; 31 | 32 | SELECT 33 | CAST(DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 4 YEAR), '%Y-01-01 00:00:00') AS DATETIME), 34 | CAST(DATE_FORMAT(DATE_ADD(NOW(), INTERVAL 1 YEAR), '%Y-12-31 23:59:59') AS DATETIME), 35 | -- Icinga 2 writes seconds with timestamp offset into columns not aware of timezones 36 | -- This is an attempt to fix those values: 37 | CASE WHEN COALESCE( 38 | (SELECT CASE WHEN program_version LIKE 'v2%' THEN 1 ELSE 0 END 39 | FROM icinga_programstatus 40 | WHERE is_currently_running = 1 41 | ORDER BY status_update_time DESC 42 | ), 43 | 1 44 | ) = 1 THEN TIMESTAMPDIFF(SECOND, UTC_TIMESTAMP(), NOW()) ELSE 0 END 45 | INTO t_start, t_end, ts_offset; 46 | 47 | OPEN cursor_tp; 48 | 49 | tp_loop: LOOP 50 | FETCH cursor_tp INTO tp_id, tpo_id; 51 | IF done THEN 52 | LEAVE tp_loop; 53 | END IF; 54 | 55 | SET @tp_lastend := NULL, 56 | @tp_lastday := NULL, 57 | @day_offset := NULL; 58 | 59 | INSERT 60 | INTO icinga_sla_periods SELECT 61 | tpo_id, 62 | DATE_ADD(CAST(monthly.date AS DATETIME), INTERVAL finaltps.start_sec SECOND) AS start_time, 63 | DATE_ADD(CAST(monthly.date AS DATETIME), INTERVAL finaltps.end_sec SECOND) AS end_time 64 | 65 | FROM ( 66 | SELECT 67 | DATE_ADD(DATE(t_start), INTERVAL @day_offset := @day_offset + 1 DAY) AS date, 68 | DAYOFWEEK(DATE_ADD(DATE(t_start), INTERVAL @day_offset DAY)) - 1 AS weekday 69 | 70 | FROM icinga_objects o 71 | JOIN (SELECT @day_offset := -1) day_offset 72 | 73 | ORDER BY object_id 74 | LIMIT 2194 75 | ) monthly JOIN ( 76 | SELECT 77 | NULL AS day, 78 | NULL as start_sec, 79 | NULL AS end_sec 80 | FROM DUAL 81 | WHERE (@tp_lastday := NULL) IS NOT NULL 82 | AND ((@tp_lastend := 0) + (@day_offset := -1)) = 1 83 | 84 | UNION 85 | 86 | SELECT 87 | day, 88 | (start_sec + ts_offset) % 86400 AS start_sec, 89 | CASE WHEN (end_sec + ts_offset) = 86400 THEN 86400 ELSE (end_sec + ts_offset) % 86400 END AS end_sec 90 | FROM icinga_timeperiod_timeranges tpr 91 | 92 | JOIN icinga_timeperiods tp ON tp.timeperiod_id = tpr.timeperiod_id 93 | WHERE tp.timeperiod_object_id = tpo_id 94 | 95 | ) finaltps ON finaltps.day = monthly.weekday 96 | WHERE DATE_ADD(CAST(monthly.date AS DATETIME), INTERVAL finaltps.end_sec - 1 SECOND) <= t_end 97 | ORDER BY monthly.date, finaltps.start_sec, finaltps.end_sec 98 | ; 99 | 100 | END LOOP tp_loop; 101 | 102 | CLOSE cursor_tp; 103 | 104 | COMMIT; 105 | SET SESSION binlog_format = STATEMENT; 106 | 107 | 108 | 109 | SELECT 0 INTO fake_result FROM icinga_objects LIMIT 1; 110 | END; 111 | $$ 112 | DELIMITER ; 113 | -------------------------------------------------------------------------------- /schema/slaperiod-schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS icinga_sla_periods; 2 | CREATE TABLE icinga_sla_periods ( 3 | timeperiod_object_id BIGINT(20) UNSIGNED NOT NULL, 4 | start_time TIMESTAMP NOT NULL DEFAULT '1970-01-02 00:00:00', 5 | end_time TIMESTAMP NOT NULL DEFAULT '1970-01-02 00:00:00', 6 | PRIMARY KEY tp_start (timeperiod_object_id, start_time), 7 | UNIQUE KEY tp_end (timeperiod_object_id, end_time) 8 | ) ENGINE InnoDB; 9 | 10 | DROP TABLE IF EXISTS icinga_outofsla_periods; 11 | CREATE TABLE icinga_outofsla_periods ( 12 | timeperiod_object_id BIGINT(20) UNSIGNED NOT NULL, 13 | start_time TIMESTAMP NOT NULL DEFAULT '1970-01-02 00:00:00', 14 | end_time TIMESTAMP NOT NULL DEFAULT '1970-01-02 00:00:00', 15 | PRIMARY KEY tp_start (timeperiod_object_id, start_time), 16 | UNIQUE KEY tp_end (timeperiod_object_id, end_time) 17 | ) ENGINE InnoDB; 18 | --------------------------------------------------------------------------------