diff options
Diffstat (limited to 'library/Icingadb/ProvidedHook')
-rw-r--r-- | library/Icingadb/ProvidedHook/ApplicationState.php | 111 | ||||
-rw-r--r-- | library/Icingadb/ProvidedHook/IcingaHealth.php | 115 | ||||
-rw-r--r-- | library/Icingadb/ProvidedHook/RedisHealth.php | 55 | ||||
-rw-r--r-- | library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php | 68 | ||||
-rw-r--r-- | library/Icingadb/ProvidedHook/Reporting/ServiceSlaReport.php | 72 | ||||
-rw-r--r-- | library/Icingadb/ProvidedHook/Reporting/SlaReport.php | 279 | ||||
-rw-r--r-- | library/Icingadb/ProvidedHook/X509/Sni.php | 57 |
7 files changed, 757 insertions, 0 deletions
diff --git a/library/Icingadb/ProvidedHook/ApplicationState.php b/library/Icingadb/ProvidedHook/ApplicationState.php new file mode 100644 index 0000000..1b6f8c1 --- /dev/null +++ b/library/Icingadb/ProvidedHook/ApplicationState.php @@ -0,0 +1,111 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\ProvidedHook; + +use Exception; +use Icinga\Application\Hook\ApplicationStateHook; +use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Common\IcingaRedis; +use Icinga\Module\Icingadb\Model\Instance; +use Icinga\Web\Session; +use ipl\Stdlib\Filter; + +class ApplicationState extends ApplicationStateHook +{ + use Database; + + public function collectMessages() + { + try { + $lastIcingaHeartbeat = IcingaRedis::getLastIcingaHeartbeat(); + } catch (Exception $e) { + $downSince = Session::getSession()->getNamespace('icingadb')->get('redis.down-since'); + + if ($downSince === null) { + $downSince = time(); + Session::getSession()->getNamespace('icingadb')->set('redis.down-since', $downSince); + } + + $this->addError( + 'icingadb/redis-down', + $downSince, + sprintf(t("Can't connect to Icinga Redis: %s"), $e->getMessage()) + ); + + return; + } + + $instance = Instance::on($this->getDb()) + ->with(['endpoint']) + ->filter(Filter::equal('responsible', true)) + ->orderBy('heartbeat', 'desc') + ->first(); + + if ($instance === null) { + $noInstanceSince = Session::getSession() + ->getNamespace('icingadb')->get('icingadb.no-instance-since'); + + if ($noInstanceSince === null) { + $noInstanceSince = time(); + Session::getSession() + ->getNamespace('icingadb')->set('icingadb.no-instance-since', $noInstanceSince); + } + + $this->addError( + 'icingadb/no-instance', + $noInstanceSince, + t( + 'It seems that Icinga DB is not running.' + . ' Make sure Icinga DB is running and writing into the database.' + ) + ); + + return; + } else { + Session::getSession()->getNamespace('icingadb')->delete('db.no-instance-since'); + } + + $outdatedDbHeartbeat = $instance->heartbeat < time() - 60; + + if ($lastIcingaHeartbeat === null) { + $missingSince = Session::getSession() + ->getNamespace('icingadb')->get('redis.heartbeat-missing-since'); + + if ($missingSince === null) { + $missingSince = time(); + Session::getSession() + ->getNamespace('icingadb')->set('redis.heartbeat-missing-since', $missingSince); + } + + $lastIcingaHeartbeat = $missingSince; + } else { + Session::getSession()->getNamespace('icingadb')->delete('redis.heartbeat-missing-since'); + } + + switch (true) { + case $outdatedDbHeartbeat && $instance->heartbeat > $lastIcingaHeartbeat: + $this->addError( + 'icingadb/redis-outdated', + $lastIcingaHeartbeat, + t('Icinga Redis is outdated. Make sure Icinga 2 is running and connected to Redis.') + ); + + break; + case $outdatedDbHeartbeat: + $this->addError( + 'icingadb/icingadb-down', + $instance->heartbeat, + t( + 'It seems that Icinga DB is not running.' + . ' Make sure Icinga DB is running and writing into the database.' + ) + ); + + break; + } + + Session::getSession()->getNamespace('icingadb')->delete('redis.down-since'); + } +} diff --git a/library/Icingadb/ProvidedHook/IcingaHealth.php b/library/Icingadb/ProvidedHook/IcingaHealth.php new file mode 100644 index 0000000..863c207 --- /dev/null +++ b/library/Icingadb/ProvidedHook/IcingaHealth.php @@ -0,0 +1,115 @@ +<?php + +// Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Icingadb\ProvidedHook; + +use Icinga\Application\Hook\HealthHook; +use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Model\Instance; +use ipl\Web\Url; + +class IcingaHealth extends HealthHook +{ + use Database; + + /** @var Instance */ + protected $instance; + + public function getName(): string + { + return 'Icinga DB'; + } + + public function getUrl(): Url + { + return Url::fromPath('icingadb/health'); + } + + public function checkHealth() + { + $instance = $this->getInstance(); + + if ($instance === null) { + $this->setState(self::STATE_UNKNOWN); + $this->setMessage(t( + 'Icinga DB is not running or not writing into the database' + . ' (make sure the icinga feature "icingadb" is enabled)' + )); + } elseif ($instance->heartbeat < time() - 60) { + $this->setState(self::STATE_CRITICAL); + $this->setMessage(t( + 'Icinga DB is not running or not writing into the database' + . ' (make sure the icinga feature "icingadb" is enabled)' + )); + } else { + $this->setState(self::STATE_OK); + $this->setMessage(t('Icinga DB is running and writing into the database')); + $warningMessages = []; + + if (! $instance->icinga2_active_host_checks_enabled) { + $this->setState(self::STATE_WARNING); + $warningMessages[] = t('Active host checks are disabled'); + } + + if (! $instance->icinga2_active_service_checks_enabled) { + $this->setState(self::STATE_WARNING); + $warningMessages[] = t('Active service checks are disabled'); + } + + if (! $instance->icinga2_notifications_enabled) { + $this->setState(self::STATE_WARNING); + $warningMessages[] = t('Notifications are disabled'); + } + + if ($this->getState() === self::STATE_WARNING) { + $this->setMessage(implode("; ", $warningMessages)); + } + } + + if ($instance !== null) { + $this->setMetrics([ + 'heartbeat' => $instance->heartbeat, + 'responsible' => $instance->responsible, + 'icinga2_active_host_checks_enabled' => $instance->icinga2_active_host_checks_enabled, + 'icinga2_active_service_checks_enabled' => $instance->icinga2_active_service_checks_enabled, + 'icinga2_event_handlers_enabled' => $instance->icinga2_event_handlers_enabled, + 'icinga2_flap_detection_enabled' => $instance->icinga2_flap_detection_enabled, + 'icinga2_notifications_enabled' => $instance->icinga2_notifications_enabled, + 'icinga2_performance_data_enabled' => $instance->icinga2_performance_data_enabled, + 'icinga2_start_time' => $instance->icinga2_start_time, + 'icinga2_version' => $instance->icinga2_version, + 'endpoint' => ['name' => $instance->endpoint->name] + ]); + } + } + + /** + * Get an Icinga DB instance + * + * @return ?Instance + */ + protected function getInstance() + { + if ($this->instance === null) { + $this->instance = Instance::on($this->getDb()) + ->with('endpoint') + ->columns([ + 'heartbeat', + 'responsible', + 'icinga2_active_host_checks_enabled', + 'icinga2_active_service_checks_enabled', + 'icinga2_event_handlers_enabled', + 'icinga2_flap_detection_enabled', + 'icinga2_notifications_enabled', + 'icinga2_performance_data_enabled', + 'icinga2_start_time', + 'icinga2_version', + 'endpoint.name' + ]) + ->first(); + } + + return $this->instance; + } +} diff --git a/library/Icingadb/ProvidedHook/RedisHealth.php b/library/Icingadb/ProvidedHook/RedisHealth.php new file mode 100644 index 0000000..9e71154 --- /dev/null +++ b/library/Icingadb/ProvidedHook/RedisHealth.php @@ -0,0 +1,55 @@ +<?php + +// Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Icingadb\ProvidedHook; + +use Exception; +use Icinga\Application\Hook\HealthHook; +use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Common\IcingaRedis; +use Icinga\Module\Icingadb\Model\Instance; + +class RedisHealth extends HealthHook +{ + use Database; + + public function getName(): string + { + return 'Icinga Redis'; + } + + public function checkHealth() + { + try { + $lastIcingaHeartbeat = IcingaRedis::getLastIcingaHeartbeat(); + if ($lastIcingaHeartbeat === null) { + $lastIcingaHeartbeat = time(); + } + + $instance = Instance::on($this->getDb())->columns('heartbeat')->first(); + + if ($instance === null) { + $this->setState(self::STATE_UNKNOWN); + $this->setMessage(t( + 'Can\'t check Icinga Redis: Icinga DB is not running or not writing into the database' + . ' (make sure the icinga feature "icingadb" is enabled)' + )); + + return; + } + + $outdatedDbHeartbeat = $instance->heartbeat < time() - 60; + if (! $outdatedDbHeartbeat || $instance->heartbeat <= $lastIcingaHeartbeat) { + $this->setState(self::STATE_OK); + $this->setMessage(t('Icinga Redis available and up to date.')); + } elseif ($instance->heartbeat > $lastIcingaHeartbeat) { + $this->setState(self::STATE_CRITICAL); + $this->setMessage(t('Icinga Redis outdated. Make sure Icinga 2 is running and connected to Redis.')); + } + } catch (Exception $e) { + $this->setState(self::STATE_CRITICAL); + $this->setMessage(sprintf(t("Can't connect to Icinga Redis: %s"), $e->getMessage())); + } + } +} diff --git a/library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php b/library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php new file mode 100644 index 0000000..d9c4f4f --- /dev/null +++ b/library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php @@ -0,0 +1,68 @@ +<?php + +/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\ProvidedHook\Reporting; + +use Icinga\Application\Icinga; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Reporting\ReportData; +use Icinga\Module\Reporting\ReportRow; +use Icinga\Module\Reporting\Timerange; +use ipl\Sql\Expression; +use ipl\Stdlib\Filter\Rule; + +use function ipl\I18n\t; + +class HostSlaReport extends SlaReport +{ + public function getName() + { + $name = t('Host SLA'); + if (Icinga::app()->getModuleManager()->hasEnabled('idoreports')) { + $name .= ' (Icinga DB)'; + } + + return $name; + } + + protected function createReportData() + { + return (new ReportData()) + ->setDimensions([t('Hostname')]) + ->setValues([t('SLA in %')]); + } + + protected function createReportRow($row) + { + if ($row->sla === null) { + return null; + } + + return (new ReportRow()) + ->setDimensions([$row->display_name]) + ->setValues([(float) $row->sla]); + } + + protected function fetchSla(Timerange $timerange, Rule $filter = null) + { + $sla = Host::on($this->getDb()) + ->columns([ + 'display_name', + 'sla' => new Expression(sprintf( + "get_sla_ok_percent(%s, NULL, '%s', '%s')", + 'host.id', + $timerange->getStart()->format('Uv'), + $timerange->getEnd()->format('Uv') + )) + ]); + + $this->applyRestrictions($sla); + + if ($filter !== null) { + $sla->filter($filter); + } + + return $sla; + } +} diff --git a/library/Icingadb/ProvidedHook/Reporting/ServiceSlaReport.php b/library/Icingadb/ProvidedHook/Reporting/ServiceSlaReport.php new file mode 100644 index 0000000..46a0684 --- /dev/null +++ b/library/Icingadb/ProvidedHook/Reporting/ServiceSlaReport.php @@ -0,0 +1,72 @@ +<?php + +/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\ProvidedHook\Reporting; + +use Icinga\Application\Icinga; +use Icinga\Module\Icingadb\Model\Service; +use Icinga\Module\Reporting\ReportData; +use Icinga\Module\Reporting\ReportRow; +use Icinga\Module\Reporting\Timerange; +use ipl\Sql\Expression; +use ipl\Stdlib\Filter\Rule; + +use function ipl\I18n\t; + +class ServiceSlaReport extends SlaReport +{ + public function getName() + { + $name = t('Service SLA'); + if (Icinga::app()->getModuleManager()->hasEnabled('idoreports')) { + $name .= ' (Icinga DB)'; + } + + return $name; + } + + protected function createReportData() + { + return (new ReportData()) + ->setDimensions([t('Hostname'), t('Service Name')]) + ->setValues([t('SLA in %')]); + } + + protected function createReportRow($row) + { + if ($row->sla === null) { + return null; + } + + return (new ReportRow()) + ->setDimensions([$row->host->display_name, $row->display_name]) + ->setValues([(float) $row->sla]); + } + + protected function fetchSla(Timerange $timerange, Rule $filter = null) + { + $sla = Service::on($this->getDb()) + ->columns([ + 'host.display_name', + 'display_name', + 'sla' => new Expression(sprintf( + "get_sla_ok_percent(%s, %s, '%s', '%s')", + 'service.host_id', + 'service.id', + $timerange->getStart()->format('Uv'), + $timerange->getEnd()->format('Uv') + )) + ]); + + $sla->resetOrderBy()->orderBy('host.display_name')->orderBy('display_name'); + + $this->applyRestrictions($sla); + + if ($filter !== null) { + $sla->filter($filter); + } + + return $sla; + } +} diff --git a/library/Icingadb/ProvidedHook/Reporting/SlaReport.php b/library/Icingadb/ProvidedHook/Reporting/SlaReport.php new file mode 100644 index 0000000..b5898fd --- /dev/null +++ b/library/Icingadb/ProvidedHook/Reporting/SlaReport.php @@ -0,0 +1,279 @@ +<?php + +/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\ProvidedHook\Reporting; + +use DateInterval; +use DatePeriod; +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Widget\EmptyState; +use Icinga\Module\Reporting\Hook\ReportHook; +use Icinga\Module\Reporting\ReportData; +use Icinga\Module\Reporting\ReportRow; +use Icinga\Module\Reporting\Timerange; +use ipl\Html\Form; +use ipl\Html\Html; +use ipl\Stdlib\Filter\Rule; +use ipl\Web\Filter\QueryString; + +use function ipl\I18n\t; + +/** + * Base class for host and service SLA reports + */ +abstract class SlaReport extends ReportHook +{ + use Auth; + use Database; + + /** @var float If an SLA value is lower than the threshold, it is considered not ok */ + const DEFAULT_THRESHOLD = 99.5; + + /** @var int The amount of decimal places for the report result */ + const DEFAULT_REPORT_PRECISION = 2; + + /** + * Create and return a {@link ReportData} container + * + * @return ReportData Container initialized with the expected dimensions and value labels for the specific report + */ + abstract protected function createReportData(); + + /** + * Create and return a {@link ReportRow} + * + * @param mixed $row Data for the row + * + * @return ReportRow|null Row with the dimensions and values for the specific report set according to the data + * expected in {@link createRepportData()} or null for no data + */ + abstract protected function createReportRow($row); + + /** + * Fetch SLA according to specified time range and filter + * + * @param Timerange $timerange + * @param Rule|null $filter + * + * @return iterable + */ + abstract protected function fetchSla(Timerange $timerange, Rule $filter = null); + + protected function fetchReportData(Timerange $timerange, array $config = null) + { + $rd = $this->createReportData(); + $rows = []; + + $filter = trim((string) $config['filter']) ?: '*'; + $filter = $filter !== '*' ? QueryString::parse($filter) : null; + + if (isset($config['breakdown']) && $config['breakdown'] !== 'none') { + switch ($config['breakdown']) { + case 'day': + $interval = new DateInterval('P1D'); + $format = 'Y-m-d'; + $boundary = 'tomorrow midnight'; + + 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; + } + + $dimensions = $rd->getDimensions(); + $dimensions[] = ucfirst($config['breakdown']); + $rd->setDimensions($dimensions); + + foreach ($this->yieldTimerange($timerange, $interval, $boundary) as list($start, $end)) { + foreach ($this->fetchSla(new Timerange($start, $end), $filter) as $row) { + $row = $this->createReportRow($row); + + if ($row === null) { + continue; + } + + $dimensions = $row->getDimensions(); + $dimensions[] = $start->format($format); + $row->setDimensions($dimensions); + + $rows[] = $row; + } + } + } else { + foreach ($this->fetchSla($timerange, $filter) as $row) { + $rows[] = $this->createReportRow($row); + } + } + + $rd->setRows($rows); + + return $rd; + } + + /** + * Yield start and end times that recur at the specified interval over the given time range + * + * @param Timerange $timerange + * @param DateInterval $interval + * @param string|null $boundary English text datetime description for calculating bounds to get + * calendar days, weeks or months instead of relative times according to interval + * + * @return \Generator + */ + protected function yieldTimerange(Timerange $timerange, DateInterval $interval, $boundary = null) + { + $start = clone $timerange->getStart(); + $end = clone $timerange->getEnd(); + $oneSecond = new DateInterval('PT1S'); + + if ($boundary !== null) { + $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; + } + + yield [$start, $end]; + } + + public function initConfigForm(Form $form) + { + $form->addElement('text', 'filter', [ + 'label' => t('Filter') + ]); + + $form->addElement('select', 'breakdown', [ + 'label' => t('Breakdown'), + 'options' => [ + 'none' => t('None', 'SLA Report Breakdown'), + 'day' => t('Day'), + 'week' => t('Week'), + 'month' => t('Month') + ] + ]); + + $form->addElement('number', 'threshold', [ + 'label' => t('Threshold'), + 'placeholder' => static::DEFAULT_THRESHOLD, + 'step' => '0.01', + 'min' => '1', + 'max' => '100' + ]); + + $form->addElement('number', 'sla_precision', [ + 'label' => t('Amount Decimal Places'), + 'placeholder' => static::DEFAULT_REPORT_PRECISION, + 'min' => '1', + 'max' => '12' + ]); + } + + public function getData(Timerange $timerange, array $config = null) + { + return $this->fetchReportData($timerange, $config); + } + + public function getHtml(Timerange $timerange, array $config = null) + { + $data = $this->getData($timerange, $config); + + if (! count($data)) { + return new EmptyState(t('No data found.')); + } + + $threshold = isset($config['threshold']) ? (float) $config['threshold'] : static::DEFAULT_THRESHOLD; + + $tableHeaderCells = []; + + foreach ($data->getDimensions() as $dimension) { + $tableHeaderCells[] = Html::tag('th', null, $dimension); + } + + foreach ($data->getValues() as $value) { + $tableHeaderCells[] = Html::tag('th', null, $value); + } + + $tableRows = []; + $precision = $config['sla_precision'] ?? static::DEFAULT_REPORT_PRECISION; + + 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, $precision)); + + $tableRows[] = Html::tag('tr', null, $cells); + } + + // We only have one average + $average = $data->getAverages()[0]; + + if ($average < $threshold) { + $slaClass = 'nok'; + } else { + $slaClass = 'ok'; + } + + $total = $this instanceof HostSlaReport + ? sprintf(t('Total (%d Hosts)'), $data->count()) + : sprintf(t('Total (%d Services)'), $data->count()); + + $tableRows[] = Html::tag('tr', null, [ + Html::tag('td', ['colspan' => count($data->getDimensions())], $total), + Html::tag('td', ['class' => "sla-column $slaClass"], round($average, $precision)) + ]); + + $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; + } +} diff --git a/library/Icingadb/ProvidedHook/X509/Sni.php b/library/Icingadb/ProvidedHook/X509/Sni.php new file mode 100644 index 0000000..d6e1415 --- /dev/null +++ b/library/Icingadb/ProvidedHook/X509/Sni.php @@ -0,0 +1,57 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\ProvidedHook\X509; + +use Generator; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\X509\Hook\SniHook; +use ipl\Web\Filter\QueryString; + +class Sni extends SniHook +{ + use Auth; + use Database; + + /** + * @inheritDoc + */ + public function getHosts(Filter $filter = null): Generator + { + $queryHost = Host::on($this->getDb()); + + $queryHost->getSelectBase(); + + $hostStatusCols = [ + 'host_name' => 'name', + 'host_address' => 'address', + 'host_address6' => 'address6' + ]; + + $queryHost = $queryHost->columns($hostStatusCols); + + $this->applyRestrictions($queryHost); + + if ($filter !== null) { + $queryString = $filter->toQueryString(); + $filterCondition = QueryString::parse($queryString); + $queryHost->filter($filterCondition); + } + + $hosts = $this->getdb()->select($queryHost->assembleSelect()); + + foreach ($hosts as $host) { + if (! empty($host->host_address)) { + yield $host->host_address => $host->host_name; + } + + if (! empty($host->host_address6)) { + yield $host->host_address6 => $host->host_name; + } + } + } +} |