summaryrefslogtreecommitdiffstats
path: root/library/Icingadb/ProvidedHook/Reporting
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icingadb/ProvidedHook/Reporting')
-rw-r--r--library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php68
-rw-r--r--library/Icingadb/ProvidedHook/Reporting/ServiceSlaReport.php72
-rw-r--r--library/Icingadb/ProvidedHook/Reporting/SlaReport.php297
-rw-r--r--library/Icingadb/ProvidedHook/Reporting/TotalHostSlaReport.php19
-rw-r--r--library/Icingadb/ProvidedHook/Reporting/TotalServiceSlaReport.php19
5 files changed, 475 insertions, 0 deletions
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..8dcc64e
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/Reporting/SlaReport.php
@@ -0,0 +1,297 @@
+<?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\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 ipl\Web\Widget\EmptyState;
+
+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;
+
+ $interval = null;
+ $boundary = null;
+ $format = null;
+ if (isset($config['breakdown']) && $config['breakdown'] !== 'none') {
+ switch ($config['breakdown']) {
+ case 'hour':
+ $interval = new DateInterval('PT1H');
+ $format = 'H:i:s';
+ $boundary = '+1 hour';
+
+ break;
+ 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'),
+ 'hour' => t('Hour'),
+ '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'
+ ]);
+
+ $form->addElement('checkbox', 'export_total', [
+ 'label' => t('Export Total Averages'),
+ 'description' => t('Export total averages to CSV and JSON'),
+ // Instead of y/n, 0/1 can be implicitly cast to bool which is done where the config is actually used.
+ 'checkedValue' => '1',
+ 'uncheckedValue' => '0'
+ ]);
+ }
+
+ 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/Reporting/TotalHostSlaReport.php b/library/Icingadb/ProvidedHook/Reporting/TotalHostSlaReport.php
new file mode 100644
index 0000000..b09ffb7
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/Reporting/TotalHostSlaReport.php
@@ -0,0 +1,19 @@
+<?php
+
+/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\ProvidedHook\Reporting;
+
+use Icinga\Module\Icingadb\Hook\Common\TotalSlaReportUtils;
+
+use function ipl\I18n\t;
+
+class TotalHostSlaReport extends HostSlaReport
+{
+ use TotalSlaReportUtils;
+
+ public function getName()
+ {
+ return t('Total Host SLA');
+ }
+}
diff --git a/library/Icingadb/ProvidedHook/Reporting/TotalServiceSlaReport.php b/library/Icingadb/ProvidedHook/Reporting/TotalServiceSlaReport.php
new file mode 100644
index 0000000..e5ebf57
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/Reporting/TotalServiceSlaReport.php
@@ -0,0 +1,19 @@
+<?php
+
+/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\ProvidedHook\Reporting;
+
+use Icinga\Module\Icingadb\Hook\Common\TotalSlaReportUtils;
+
+use function ipl\I18n\t;
+
+class TotalServiceSlaReport extends ServiceSlaReport
+{
+ use TotalSlaReportUtils;
+
+ public function getName()
+ {
+ return t('Total Service SLA');
+ }
+}