diff options
Diffstat (limited to 'library')
-rw-r--r-- | library/Idoreports/HostSlaReport.php | 104 | ||||
-rw-r--r-- | library/Idoreports/IdoReport.php | 257 | ||||
-rw-r--r-- | library/Idoreports/ServiceSlaReport.php | 104 |
3 files changed, 465 insertions, 0 deletions
diff --git a/library/Idoreports/HostSlaReport.php b/library/Idoreports/HostSlaReport.php new file mode 100644 index 0000000..70cbc1b --- /dev/null +++ b/library/Idoreports/HostSlaReport.php @@ -0,0 +1,104 @@ +<?php + +// Icinga IDO Reports | (c) 2018 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Idoreports; + +use Icinga\Module\Reporting\ReportData; +use Icinga\Module\Reporting\ReportRow; +use Icinga\Module\Reporting\Timerange; +use ipl\Html\Form; + +class HostSlaReport extends IdoReport +{ + public function getName() + { + return 'Host SLA'; + } + + public function initConfigForm(Form $form) + { + $form->addElement('text', 'filter', [ + 'label' => 'Filter' + ]); + + $form->addElement('select', 'breakdown', [ + 'label' => 'Breakdown', + 'options' => [ + 'none' => 'None', + 'day' => 'Day', + 'week' => 'Week', + 'month' => 'Month' + ] + ]); + + $form->addElement('number', 'threshold', [ + 'label' => 'Threshold', + 'placeholder' => '99.5', + 'step' => '0.01', + 'min' => '1', + 'max' => '100' + ]); + } + + protected function fetchSla(Timerange $timerange, array $config = null) + { + $rd = new ReportData(); + + if (isset($config['breakdown']) && $config['breakdown'] !== 'none') { + switch ($config['breakdown']) { + case 'day': + $interval = new \DateInterval('P1D'); + $format = 'Y-m-d'; + $boundary = false; + break; + case 'week': + $interval = new \DateInterval('P1W'); + $format = 'Y-\WW'; + $boundary = 'monday next week midnight'; + break; + case 'month': + $interval = new \DateInterval('P1M'); + $format = 'Y-m'; + $boundary = 'first day of next month midnight'; + break; + } + + $rd + ->setDimensions(['Hostname', ucfirst($config['breakdown'])]) + ->setValues(['SLA in %']); + + $rows = []; + + foreach ($this->yieldTimerange($timerange, $interval, $boundary) as list($start, $end)) { + foreach ($this->fetchHostSla(new Timerange($start, $end), $config) as $row) { + if ($row->sla === null) { + continue; + } + + $rows[] = (new ReportRow()) + ->setDimensions([$row->host_display_name, $start->format($format)]) + ->setValues([(float) $row->sla]); + } + } + + $rd->setRows($rows); + } else { + $rd + ->setDimensions(['Hostname']) + ->setValues(['SLA in %']); + + $rows = []; + + foreach ($this->fetchHostSla($timerange, $config) as $row) { + $rows[] = (new ReportRow()) + ->setDimensions([$row->host_display_name]) + ->setValues([(float) $row->sla]); + } + + $rd->setRows($rows); + } + + return $rd; + } +} diff --git a/library/Idoreports/IdoReport.php b/library/Idoreports/IdoReport.php new file mode 100644 index 0000000..36e2d17 --- /dev/null +++ b/library/Idoreports/IdoReport.php @@ -0,0 +1,257 @@ +<?php + +// Icinga IDO Reports | (c) 2018 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Idoreports; + +use Icinga\Application\Icinga; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filterable; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\QueryException; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Module\Reporting\Hook\ReportHook; +use Icinga\Module\Reporting\ReportData; +use Icinga\Module\Reporting\Timerange; +use ipl\Html\Html; + +/** + * @TODO(el): Respect restrictions from monitoring module + */ +abstract class IdoReport extends ReportHook +{ + public function getData(Timerange $timerange, array $config = null) + { + return $this->fetchSla($timerange, $config); + } + + public function getHtml(Timerange $timerange, array $config = null) + { + $data = $this->fetchSla($timerange, $config); + + if (! count($data)) { + return Html::tag('p', 'No data found.'); + } + + $threshold = isset($config['threshold']) ? (float) $config['threshold'] : 99.5; + + $tableHeaderCells = []; + + foreach ($data->getDimensions() as $dimension) { + $tableHeaderCells[] = Html::tag('th', null, $dimension); + } + + foreach ($data->getValues() as $value) { + $tableHeaderCells[] = Html::tag('th', null, $value); + } + + $tableRows = []; + + foreach ($data->getRows() as $row) { + $cells = []; + + foreach ($row->getDimensions() as $dimension) { + $cells[] = Html::tag('td', null, $dimension); + } + + // We only have one metric + $sla = $row->getValues()[0]; + + if ($sla < $threshold) { + $slaClass = 'nok'; + } else { + $slaClass = 'ok'; + } + + $cells[] = Html::tag('td', ['class' => "sla-column $slaClass"], \round($sla, 2)); + + $tableRows[] = Html::tag('tr', null, $cells); + } + + // We only have one average + $average = $data->getAverages()[0]; + + if ($average < $threshold) { + $slaClass = 'nok'; + } else { + $slaClass = 'ok'; + } + + $tableRows[] = Html::tag('tr', null, [ + Html::tag('td', ['colspan' => count($data->getDimensions())], 'Total'), + Html::tag('td', ['class' => "sla-column $slaClass"], \round($average, 2)) + ]); + + $table = Html::tag( + 'table', + ['class' => 'common-table sla-table'], + [ + Html::tag( + 'thead', + null, + Html::tag( + 'tr', + null, + $tableHeaderCells + ) + ), + Html::tag('tbody', null, $tableRows) + ] + ); + + return $table; + } + + /** + * @param Timerange $timerange + * @param array $config + * + * @return ReportData + */ + abstract protected function fetchSla(Timerange $timerange, array $config = null); + + protected function applyFilterAndRestrictions($filter, Filterable $filterable) + { + $filters = Filter::matchAll(); + $filters->setAllowedFilterColumns(array( + 'host_name', + 'hostgroup_name', + 'instance_name', + 'service_description', + 'servicegroup_name', + function ($c) { + return \preg_match('/^_(?:host|service)_/i', $c); + } + )); + + try { + if ($filter !== '*') { + $filters->addFilter(Filter::fromQueryString($filter)); + } + + foreach ($this->yieldMonitoringRestrictions() as $filter) { + $filters->addFilter($filter); + } + } catch (QueryException $e) { + throw new ConfigurationError( + 'Cannot apply filter. You can only use the following columns: %s', + implode(', ', array( + 'instance_name', + 'host_name', + 'hostgroup_name', + 'service_description', + 'servicegroup_name', + '_(host|service)_<customvar-name>' + )), + $e + ); + } + + $filterable->applyFilter($filters); + } + + protected function getBackend() + { + MonitoringBackend::clearInstances(); + + return MonitoringBackend::instance(); + } + + protected function getRestrictions($name) + { + $app = Icinga::app(); + if (! $app->isCli()) { + $result = $app->getRequest()->getUser()->getRestrictions($name); + } else { + $result = []; + } + + return $result; + } + + protected function fetchHostSla(Timerange $timerange, array $config) + { + $sla = $this->getBackend()->select()->from('hoststatus', ['host_display_name'])->order('host_display_name'); + + $this->applyFilterAndRestrictions($config['filter'] ?: '*', $sla); + + /** @var \Zend_Db_Select $select */ + $select = $sla->getQuery()->getSelectQuery(); + + $columns = $sla->getQuery()->getColumns(); + $columns['sla'] = new \Zend_Db_Expr(\sprintf( + "idoreports_get_sla_ok_percent(%s, '%s', '%s', NULL)", + 'ho.object_id', + $timerange->getStart()->format('Y-m-d H:i:s'), + $timerange->getEnd()->format('Y-m-d H:i:s') + )); + + $select->columns($columns); + + return $this->getBackend()->getResource()->getDbAdapter()->query($select); + } + + protected function fetchServiceSla(Timerange $timerange, array $config) + { + $sla = $this + ->getBackend() + ->select() + ->from('servicestatus', ['host_display_name', 'service_display_name']) + ->order('host_display_name'); + + $this->applyFilterAndRestrictions($config['filter'] ?: '*', $sla); + + /** @var \Zend_Db_Select $select */ + $select = $sla->getQuery()->getSelectQuery(); + + $columns = $sla->getQuery()->getColumns(); + $columns['sla'] = new \Zend_Db_Expr(\sprintf( + "idoreports_get_sla_ok_percent(%s, '%s', '%s', NULL)", + 'so.object_id', + $timerange->getStart()->format('Y-m-d H:i:s'), + $timerange->getEnd()->format('Y-m-d H:i:s') + )); + + $select->columns($columns); + + return $this->getBackend()->getResource()->getDbAdapter()->query($select); + } + + protected function yieldMonitoringRestrictions() + { + foreach ($this->getRestrictions('monitoring/filter/objects') as $restriction) { + if ($restriction !== '*') { + yield Filter::fromQueryString($restriction); + } + } + } + + protected function yieldTimerange(Timerange $timerange, \DateInterval $interval, $boundary) + { + $start = clone $timerange->getStart(); + $end = clone $timerange->getEnd(); + $oneSecond = new \DateInterval('PT1S'); + + if ($boundary !== false) { + $intermediate = (clone $start)->modify($boundary); + if ($intermediate < $end) { + yield [clone $start, $intermediate->sub($oneSecond)]; + + $start->modify($boundary); + } + } + + $period = new \DatePeriod($start, $interval, $end, \DatePeriod::EXCLUDE_START_DATE); + + foreach ($period as $date) { + /** @var \DateTime $date */ + + yield [$start, (clone $date)->sub($oneSecond)]; + + $start = $date; + } + + // @TODO(el): Should we add if ($start < $end) here to protect us from invalid ranges? + yield [$start, $end]; + } +} diff --git a/library/Idoreports/ServiceSlaReport.php b/library/Idoreports/ServiceSlaReport.php new file mode 100644 index 0000000..7127736 --- /dev/null +++ b/library/Idoreports/ServiceSlaReport.php @@ -0,0 +1,104 @@ +<?php + +// Icinga IDO Reports | (c) 2018 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Idoreports; + +use Icinga\Module\Reporting\ReportData; +use Icinga\Module\Reporting\ReportRow; +use Icinga\Module\Reporting\Timerange; +use ipl\Html\Form; + +class ServiceSlaReport extends IdoReport +{ + public function getName() + { + return 'Service SLA'; + } + + public function initConfigForm(Form $form) + { + $form->addElement('text', 'filter', [ + 'label' => 'Filter' + ]); + + $form->addElement('select', 'breakdown', [ + 'label' => 'Breakdown', + 'options' => [ + 'none' => 'None', + 'day' => 'Day', + 'week' => 'Week', + 'month' => 'Month' + ] + ]); + + $form->addElement('number', 'threshold', [ + 'label' => 'Threshold', + 'placeholder' => '99.5', + 'step' => '0.01', + 'min' => '1', + 'max' => '100' + ]); + } + + protected function fetchSla(Timerange $timerange, array $config = null) + { + $rd = new ReportData(); + + if (isset($config['breakdown']) && $config['breakdown'] !== 'none') { + switch ($config['breakdown']) { + case 'day': + $interval = new \DateInterval('P1D'); + $format = 'Y-m-d'; + $boundary = false; + break; + case 'week': + $interval = new \DateInterval('P1W'); + $format = 'Y-\WW'; + $boundary = 'monday next week midnight'; + break; + case 'month': + $interval = new \DateInterval('P1M'); + $format = 'Y-m'; + $boundary = 'first day of next month midnight'; + break; + } + + $rd + ->setDimensions(['Hostname', 'Service Name', ucfirst($config['breakdown'])]) + ->setValues(['SLA in %']); + + $rows = []; + + foreach ($this->yieldTimerange($timerange, $interval, $boundary) as list($start, $end)) { + foreach ($this->fetchServiceSla(new Timerange($start, $end), $config) as $row) { + if ($row->sla === null) { + continue; + } + + $rows[] = (new ReportRow()) + ->setDimensions([$row->host_display_name, $row->service_display_name, $start->format($format)]) + ->setValues([(float) $row->sla]); + } + } + + $rd->setRows($rows); + } else { + $rd + ->setDimensions(['Hostname', 'Service Name']) + ->setValues(['SLA in %']); + + $rows = []; + + foreach ($this->fetchServiceSla($timerange, $config) as $row) { + $rows[] = (new ReportRow()) + ->setDimensions([$row->host_display_name, $row->service_display_name]) + ->setValues([(float) $row->sla]); + } + + $rd->setRows($rows); + } + + return $rd; + } +} |