diff --git a/library/Icingadb/Model/Host.php b/library/Icingadb/Model/Host.php index a76cb3655..f8307566a 100644 --- a/library/Icingadb/Model/Host.php +++ b/library/Icingadb/Model/Host.php @@ -222,6 +222,11 @@ public function createRelations(Relations $relations) $relations->belongsToMany('hostgroup', Hostgroup::class) ->through(HostgroupMember::class); + $relations->hasMany('sla_history_state', SlaHistoryState::class) + ->setJoinType('LEFT'); + $relations->hasMany('sla_history_downtime', SlaHistoryDowntime::class) + ->setJoinType('LEFT'); + $relations->hasOne('state', HostState::class)->setJoinType('LEFT'); $relations->hasMany('comment', Comment::class)->setJoinType('LEFT'); $relations->hasMany('downtime', Downtime::class)->setJoinType('LEFT'); diff --git a/library/Icingadb/Model/HostSlaHistorySummary.php b/library/Icingadb/Model/HostSlaHistorySummary.php new file mode 100644 index 000000000..f4cf787f6 --- /dev/null +++ b/library/Icingadb/Model/HostSlaHistorySummary.php @@ -0,0 +1,108 @@ + 'host.display_name', + 'event_priority' => new Expression('1'), + 'event_time' => 'sla_history_downtime.downtime_start', + 'event_type' => new Expression('\'downtime_start\''), + 'hard_state' => new Expression('NULL'), + 'previous_hard_state' => new Expression('NULL'), + 'host_id' => 'id' + ] + ], + [ + Host::class, + [ + 'sla_history_downtime' + ], + [ + 'display_name' => 'host.display_name', + 'event_priority' => new Expression('2'), + 'event_time' => 'sla_history_downtime.downtime_end', + 'event_type' => new Expression('\'downtime_end\''), + 'hard_state' => new Expression('NULL'), + 'previous_hard_state' => new Expression('NULL'), + 'host_id' => 'id' + ] + ], + [ + Host::class, + [ + 'sla_history_state' + ], + [ + 'display_name' => 'display_name', + 'event_priority' => new Expression('0'), + 'event_time' => 'sla_history_state.event_time', + 'event_type' => new Expression('\'state_change\''), + 'hard_state' => 'sla_history_state.hard_state', + 'previous_hard_state' => 'sla_history_state.previous_hard_state', + 'host_id' => 'id' + ] + ], + // Fake result!! + [ + Host::class, + [], + [ + 'display_name' => 'host.display_name', + 'event_priority' => new Expression('3'), + 'event_time' => new Expression('NULL'), + 'event_type' => new Expression('\'end\''), + 'hard_state' => new Expression('NULL'), + 'previous_hard_state' => new Expression('NULL'), + 'host_id' => 'id' + ] + ] + ]; + } + + public function getDefaultSort() + { + return ['display_name', 'event_time', 'event_priority']; + } + + public function createRelations(Relations $relations) + { + (new Host())->createRelations($relations); + } +} diff --git a/library/Icingadb/Model/SlaHistoryDowntime.php b/library/Icingadb/Model/SlaHistoryDowntime.php new file mode 100644 index 000000000..66c5049e3 --- /dev/null +++ b/library/Icingadb/Model/SlaHistoryDowntime.php @@ -0,0 +1,59 @@ +add(new Binary([ + 'id', + 'environment_id', + 'endpoint_id', + 'host_id', + 'service_id', + 'downtime_id' + ])); + + $behaviors->add(new Timestamp([ + 'downtime_start', + 'downtime_end' + ])); + } + + public function createRelations(Relations $relations) + { + $relations->belongsTo('host', Host::class); + $relations->belongsTo('service', Service::class); + } +} diff --git a/library/Icingadb/Model/SlaHistoryState.php b/library/Icingadb/Model/SlaHistoryState.php new file mode 100644 index 000000000..1f1e8c50a --- /dev/null +++ b/library/Icingadb/Model/SlaHistoryState.php @@ -0,0 +1,60 @@ +add(new Binary([ + 'id', + 'environment_id', + 'endpoint_id', + 'host_id', + 'service_id' + ])); + + $behaviors->add(new Timestamp(['event_time'])); + } + + public function createRelations(Relations $relations) + { + $relations->belongsTo('host', Host::class); + $relations->belongsTo('service', Service::class); + } + + public function getDefaultSort() + { + return ['event_time']; + } +} diff --git a/library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php b/library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php index d9c4f4f13..086a139e6 100644 --- a/library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php +++ b/library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php @@ -5,11 +5,12 @@ namespace Icinga\Module\Icingadb\ProvidedHook\Reporting; use Icinga\Application\Icinga; -use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\HostSlaHistorySummary; use Icinga\Module\Reporting\ReportData; use Icinga\Module\Reporting\ReportRow; use Icinga\Module\Reporting\Timerange; use ipl\Sql\Expression; +use ipl\Stdlib\Filter; use ipl\Stdlib\Filter\Rule; use function ipl\I18n\t; @@ -46,23 +47,52 @@ protected function createReportRow($row) 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); + $start = (int) $timerange->getStart()->format('U'); + $end = (int) $timerange->getEnd()->format('U'); + + $query = HostSlaHistorySummary::on($this->getDb()); + + $index = 0; + foreach ($query->getUnions() as $union) { + $filterAll = Filter::all(); + if ($index >= 2) { + if ($index < 3) { + $filterAll + ->add(Filter::unlike('sla_history_state.service_id', '*')) + ->add(Filter::greaterThan('sla_history_state.event_time', $start)) + ->add(Filter::lessThan('sla_history_state.event_time', $end)); + } else { + $union->columns(array_merge($union->getColumns(), ['event_time' => new Expression($end)])); + } + } else { + $filterAll + ->add(Filter::unlike('sla_history_downtime.service_id', '*')) + ->add(Filter::lessThan('sla_history_downtime.downtime_start', $end)) + ->add(Filter::greaterThanOrEqual('sla_history_downtime.downtime_end', $start)); + + if ($index === 1) { + $filterAll->add(Filter::lessThan('sla_history_downtime.downtime_end', $end)); + } else { + $union->columns( + array_merge( + $union->getColumns(), + ['event_time' => new Expression(sprintf('GREATEST(host_sla_history_downtime.downtime_start, %s)', $start))] + ) + ); + } + } + + ++$index; + + $union->filter($filterAll); + + if ($filter !== null) { + $union->filter($filter); + } } - return $sla; + $this->applyRestrictions($query); + + return $query; } } diff --git a/library/Icingadb/ProvidedHook/Reporting/SlaReport.php b/library/Icingadb/ProvidedHook/Reporting/SlaReport.php index b5898fd83..4eb7d23f1 100644 --- a/library/Icingadb/ProvidedHook/Reporting/SlaReport.php +++ b/library/Icingadb/ProvidedHook/Reporting/SlaReport.php @@ -8,13 +8,17 @@ use DatePeriod; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Model\HostState; +use Icinga\Module\Icingadb\Model\SlaHistoryState; 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\SlaTimeline; use Icinga\Module\Reporting\Timerange; use ipl\Html\Form; use ipl\Html\Html; +use ipl\Stdlib\Filter; use ipl\Stdlib\Filter\Rule; use ipl\Web\Filter\QueryString; @@ -96,18 +100,124 @@ protected function fetchReportData(Timerange $timerange, array $config = null) $rd->setDimensions($dimensions); foreach ($this->yieldTimerange($timerange, $interval, $boundary) as list($start, $end)) { + $reports = []; + $lastHardState = 0; foreach ($this->fetchSla(new Timerange($start, $end), $filter) as $row) { - $row = $this->createReportRow($row); + if (isset($reports[$row->display_name])) { + $timeline = $reports[$row->display_name]; + } else { + $hardStateQuery = SlaHistoryState::on($this->getDb()) + ->columns(['hard_state']) + ->filter( + Filter::all( + Filter::equal('host_id', $row->host_id), + Filter::unlike('service_id', '*'), + Filter::lessThanOrEqual('event_time', $start->format('U')) + ) + ) + ->resetOrderBy() + ->orderBy('event_time', 'DESC') + ->limit(1); + + $hardStateQuery = $hardStateQuery->first(); + if (! $hardStateQuery) { + $hardStateQuery = SlaHistoryState::on($this->getDb()) + ->columns(['previous_hard_state']) + ->orderBy('event_time', 'ASC') + ->filter( + Filter::all( + Filter::equal('host_id', $row->host_id), + Filter::unlike('service_id', '*'), + Filter::greaterThan('event_time', $start->format('U')) + ) + ); + + $hardStateQuery = $hardStateQuery->first(); + if (! $hardStateQuery) { + $hardStateQuery = HostState::on($this->getDb()) + ->columns(['hard_state']) + ->filter(Filter::equal('host_id', $row->host_id)); + + $hardStateQuery = $hardStateQuery->first(); + } + } + + if ($hardStateQuery) { + $lastHardState = ! isset($hardStateQuery->hard_state) + ? (int) $hardStateQuery->previous_hard_state + : (int) $hardStateQuery->hard_state; + } + + $timeline = new SlaTimeline(); + } + + $timeline + ->addEvent($row->event_type) + ->addTime((int) $row->event_time) + ->addState($lastHardState) + ->addPreviousState((int) $row->previous_hard_state); - if ($row === null) { + if ($row->event_type === 'state_change') { + $lastHardState = (int) $row->hard_state; + } + + $reports[$row->display_name] = $timeline; + } + + if (! $rd->getInitialStartTime()) { + $rd->setInitialStartTime(((int) $start->format('Uv')) - 1000); + } + + foreach ($reports as $name => $timeline) { + if (! $rd->hasTimeline($name) && $timeline->count() <= 1 && $timeline->getEvent()[0] === 'end') { + // No data available continue; } - $dimensions = $row->getDimensions(); + $problemTime = 0; + $activeDowntimes = 0; + $lastEventTime = $rd->getInitialStartTime(); + $totalTime = (int) $end->format('Uv') - $lastEventTime; + + for ($i = 0; $i < count($timeline->getTime()); ++$i) { + if ($timeline->getEvent()[$i] === 'end') { + continue; + } + + $time = $timeline->getTime()[$i]; + $state = $timeline->getState()[$i]; + $event = $timeline->getEvent()[$i]; + + if ($timeline->getPreviousState()[$i] === 99 || ($state === 99 && $event !== 'state_change')) { + $totalTime -= $time - $lastEventTime; + } elseif ($state > 0 && $state !== 99 && $activeDowntimes === 0) { + $problemTime += $time - $lastEventTime; + } + + $lastEventTime = $time; + if ($event === 'downtime_start') { + ++$activeDowntimes; + } elseif ($event === 'downtime_end') { + --$activeDowntimes; + } + } + + if ($totalTime <= 0) { + continue; + } + + $timeline->setResult(100 * ($totalTime - $problemTime) / $totalTime); + $rd->addTimeline($name, $timeline); + + $report = (new ReportRow()) + ->setDimensions([$name]) + ->setValues([$timeline->getResult()]); + + $dimensions = $report->getDimensions(); $dimensions[] = $start->format($format); - $row->setDimensions($dimensions); + $report->setDimensions($dimensions); - $rows[] = $row; + $rows[] = $report; } } } else { @@ -240,7 +350,11 @@ public function getHtml(Timerange $timerange, array $config = null) } // We only have one average - $average = $data->getAverages()[0]; + if (strpos($this->getName(), 'Icinga DB') !== false) { + $average = $data->getIcingaDBAvg(); + } else { + $average = $data->getAverages()[0]; + } if ($average < $threshold) { $slaClass = 'nok'; @@ -274,6 +388,7 @@ public function getHtml(Timerange $timerange, array $config = null) ] ); + echo nl2br($data->getTimelineString()); return $table; } }