|
6 | 6 |
|
7 | 7 | use DateInterval; |
8 | 8 | use DatePeriod; |
| 9 | +use DateTime; |
9 | 10 | use Icinga\Module\Icingadb\Common\Auth; |
10 | 11 | use Icinga\Module\Icingadb\Common\Database; |
| 12 | +use Icinga\Module\Icingadb\Model\HostSlaHistory; |
| 13 | +use Icinga\Module\Icingadb\Model\HostState; |
| 14 | +use Icinga\Module\Icingadb\Model\ServiceSlaHistory; |
| 15 | +use Icinga\Module\Icingadb\Model\ServiceState; |
| 16 | +use Icinga\Module\Icingadb\Model\SlaHistoryState; |
11 | 17 | use Icinga\Module\Icingadb\Widget\EmptyState; |
12 | 18 | use Icinga\Module\Reporting\Hook\ReportHook; |
13 | 19 | use Icinga\Module\Reporting\ReportData; |
14 | 20 | use Icinga\Module\Reporting\ReportRow; |
| 21 | +use Icinga\Module\Reporting\SlaTimeline; |
15 | 22 | use Icinga\Module\Reporting\Timerange; |
16 | 23 | use ipl\Html\Form; |
17 | 24 | use ipl\Html\Html; |
| 25 | +use ipl\Orm\Query; |
| 26 | +use ipl\Sql\Expression; |
| 27 | +use ipl\Stdlib\Filter; |
18 | 28 | use ipl\Stdlib\Filter\Rule; |
| 29 | +use ipl\Stdlib\Str; |
19 | 30 | use ipl\Web\Filter\QueryString; |
20 | 31 |
|
21 | 32 | use function ipl\I18n\t; |
@@ -59,7 +70,78 @@ abstract protected function createReportRow($row); |
59 | 70 | * |
60 | 71 | * @return iterable |
61 | 72 | */ |
62 | | - abstract protected function fetchSla(Timerange $timerange, Rule $filter = null); |
| 73 | + protected function fetchSla(Timerange $timerange, Rule $filter = null): Query |
| 74 | + { |
| 75 | + $start = $timerange->getStart(); |
| 76 | + $end = $timerange->getEnd(); |
| 77 | + $isHostQuery = $this instanceof HostSlaReport; |
| 78 | + if ($isHostQuery) { |
| 79 | + $query = HostSlaHistory::on($this->getDb()); |
| 80 | + } else { |
| 81 | + $query = ServiceSlaHistory::on($this->getDb()); |
| 82 | + } |
| 83 | + |
| 84 | + $index = 0; |
| 85 | + foreach ($query->getUnions() as $union) { |
| 86 | + $filterAll = Filter::all(); |
| 87 | + if ($index >= 2) { |
| 88 | + if ($index < 3) { |
| 89 | + if ($isHostQuery) { |
| 90 | + $filterAll->add(Filter::unlike('sla_history_state.service_id', '*')); |
| 91 | + } |
| 92 | + |
| 93 | + $filterAll |
| 94 | + ->add(Filter::greaterThan('sla_history_state.event_time', $start)) |
| 95 | + ->add(Filter::lessThan('sla_history_state.event_time', $end)); |
| 96 | + } else { |
| 97 | + $union->columns( |
| 98 | + array_merge($union->getColumns(), [ |
| 99 | + 'event_time' => new Expression($end->format('Uv')) |
| 100 | + ]) |
| 101 | + ); |
| 102 | + } |
| 103 | + } else { |
| 104 | + if ($isHostQuery) { |
| 105 | + $filterAll->add(Filter::unlike('sla_history_downtime.service_id', '*')); |
| 106 | + } |
| 107 | + |
| 108 | + $filterAll |
| 109 | + ->add(Filter::lessThan('sla_history_downtime.downtime_start', $end)) |
| 110 | + ->add(Filter::greaterThanOrEqual('sla_history_downtime.downtime_end', $start)); |
| 111 | + |
| 112 | + if ($index === 1) { |
| 113 | + $filterAll->add(Filter::lessThan('sla_history_downtime.downtime_end', $end)); |
| 114 | + } else { |
| 115 | + $union->columns( |
| 116 | + array_merge( |
| 117 | + $union->getColumns(), |
| 118 | + [ |
| 119 | + 'event_time' => new Expression( |
| 120 | + sprintf( |
| 121 | + 'GREATEST(%s_sla_history_downtime.downtime_start, %s)', |
| 122 | + $isHostQuery ? 'host' : 'service', |
| 123 | + $start->format('Uv') |
| 124 | + ) |
| 125 | + ) |
| 126 | + ] |
| 127 | + ) |
| 128 | + ); |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + ++$index; |
| 133 | + |
| 134 | + $union->filter($filterAll); |
| 135 | + |
| 136 | + if ($filter !== null) { |
| 137 | + $union->filter($filter); |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + $this->applyRestrictions($query); |
| 142 | + |
| 143 | + return $query; |
| 144 | + } |
63 | 145 |
|
64 | 146 | protected function fetchReportData(Timerange $timerange, array $config = null) |
65 | 147 | { |
@@ -95,24 +177,93 @@ protected function fetchReportData(Timerange $timerange, array $config = null) |
95 | 177 | $dimensions[] = ucfirst($config['breakdown']); |
96 | 178 | $rd->setDimensions($dimensions); |
97 | 179 |
|
| 180 | + $isHostQuery = $this instanceof HostSlaReport; |
98 | 181 | foreach ($this->yieldTimerange($timerange, $interval, $boundary) as list($start, $end)) { |
| 182 | + $reports = []; |
99 | 183 | foreach ($this->fetchSla(new Timerange($start, $end), $filter) as $row) { |
100 | | - $row = $this->createReportRow($row); |
101 | | - |
102 | | - if ($row === null) { |
| 184 | + $key = $isHostQuery ? $row->display_name : $row->host_display_name . '!' . $row->display_name; |
| 185 | + if ( |
| 186 | + $row->event_type === 'end' |
| 187 | + && ( |
| 188 | + ! isset($reports[$key]) |
| 189 | + && ! $rd->hasTimeline($key) |
| 190 | + ) |
| 191 | + ) { |
| 192 | + // No data available |
103 | 193 | continue; |
104 | 194 | } |
105 | 195 |
|
106 | | - $dimensions = $row->getDimensions(); |
| 196 | + if (isset($reports[$key])) { |
| 197 | + $timeline = $reports[$key]; |
| 198 | + } else { |
| 199 | + $timeline = new SlaTimeline($start, $end); |
| 200 | + $serviceId = null; |
| 201 | + if (! $this instanceof HostSlaReport) { |
| 202 | + $serviceId = $row->service_id; |
| 203 | + } |
| 204 | + |
| 205 | + $timeline->setInitialHardState( |
| 206 | + $this->fetchInitialHardState($start, $row->host_id, $serviceId) |
| 207 | + ); |
| 208 | + } |
| 209 | + |
| 210 | + $timeline |
| 211 | + ->addEvent($row->event_type) |
| 212 | + ->addTime((int) $row->event_time) |
| 213 | + ->addState($row->hard_state) |
| 214 | + ->addPreviousState($row->previous_hard_state); |
| 215 | + |
| 216 | + $reports[$key] = $timeline; |
| 217 | + } |
| 218 | + |
| 219 | + foreach ($reports as $name => $timeline) { |
| 220 | + $rd->addTimeline($name, $timeline); |
| 221 | + $row = (object) []; |
| 222 | + $row->sla = $timeline->getResult(); |
| 223 | + $row->display_name = $name; |
| 224 | + |
| 225 | + if (strpos($name, '!') !== false) { |
| 226 | + list($host, $service) = Str::trimSplit($name, '!'); |
| 227 | + $row->display_name = $service; |
| 228 | + $row->host_display_name = $host; |
| 229 | + } |
| 230 | + |
| 231 | + $report = $this->createReportRow($row); |
| 232 | + $dimensions = $report->getDimensions(); |
107 | 233 | $dimensions[] = $start->format($format); |
108 | | - $row->setDimensions($dimensions); |
| 234 | + $report->setDimensions($dimensions); |
109 | 235 |
|
110 | | - $rows[] = $row; |
| 236 | + $rows[] = $report; |
111 | 237 | } |
112 | 238 | } |
113 | 239 | } else { |
114 | 240 | foreach ($this->fetchSla($timerange, $filter) as $row) { |
115 | | - $rows[] = $this->createReportRow($row); |
| 241 | + if ($rd->hasTimeline($row->display_name)) { |
| 242 | + $timeline = $rd->getTimeline($row->display_name)[0]; |
| 243 | + } else { |
| 244 | + $timeline = new SlaTimeline($timerange->getStart(), $timerange->getEnd()); |
| 245 | + $serviceId = null; |
| 246 | + if (! $this instanceof HostSlaReport) { |
| 247 | + $serviceId = $row->service_id; |
| 248 | + } |
| 249 | + |
| 250 | + $timeline->setInitialHardState( |
| 251 | + $this->fetchInitialHardState($timerange->getStart(), $row->host_id, $serviceId) |
| 252 | + ); |
| 253 | + } |
| 254 | + |
| 255 | + $timeline |
| 256 | + ->addEvent($row->event_type) |
| 257 | + ->addTime((int) $row->event_time) |
| 258 | + ->addState($row->hard_state) |
| 259 | + ->addPreviousState($row->previous_hard_state); |
| 260 | + |
| 261 | + $rd->setTimeline($row->display_name, $timeline); |
| 262 | + |
| 263 | + if ($row->event_type === 'end') { |
| 264 | + $row->sla = $timeline->getResult(); |
| 265 | + $rows[] = $this->createReportRow($row); |
| 266 | + } |
116 | 267 | } |
117 | 268 | } |
118 | 269 |
|
@@ -149,7 +300,7 @@ protected function yieldTimerange(Timerange $timerange, DateInterval $interval, |
149 | 300 | $period = new DatePeriod($start, $interval, $end, DatePeriod::EXCLUDE_START_DATE); |
150 | 301 |
|
151 | 302 | foreach ($period as $date) { |
152 | | - /** @var \DateTime $date */ |
| 303 | + /** @var DateTime $date */ |
153 | 304 | yield [$start, (clone $date)->sub($oneSecond)]; |
154 | 305 |
|
155 | 306 | $start = $date; |
@@ -240,7 +391,11 @@ public function getHtml(Timerange $timerange, array $config = null) |
240 | 391 | } |
241 | 392 |
|
242 | 393 | // We only have one average |
243 | | - $average = $data->getAverages()[0]; |
| 394 | + if (strpos($this->getName(), 'Icinga DB') !== false) { |
| 395 | + $average = $data->getIcingaDBAvg(); |
| 396 | + } else { |
| 397 | + $average = $data->getAverages()[0]; |
| 398 | + } |
244 | 399 |
|
245 | 400 | if ($average < $threshold) { |
246 | 401 | $slaClass = 'nok'; |
@@ -274,6 +429,74 @@ public function getHtml(Timerange $timerange, array $config = null) |
274 | 429 | ] |
275 | 430 | ); |
276 | 431 |
|
| 432 | + // echo '<pre>' . nl2br($data->getTimelineString()) . '</pre>'; |
277 | 433 | return $table; |
278 | 434 | } |
| 435 | + |
| 436 | + /** |
| 437 | + * Get the initial hard state of the given host/service object |
| 438 | + * |
| 439 | + * @param DateTime $start The start time of the generated sla |
| 440 | + * @param string $hostId Host binary/hex id to fetch the initial hard state for |
| 441 | + * |
| 442 | + * @return int |
| 443 | + */ |
| 444 | + protected function fetchInitialHardState(DateTime $start, string $hostId, string $serviceId = null): int |
| 445 | + { |
| 446 | + // Use OK/UP as initial hard state, when neither of the following queries could determine a correct state |
| 447 | + $initialHardState = 0; |
| 448 | + $start = $start->format('U'); |
| 449 | + |
| 450 | + $serviceFilter = $serviceId === null |
| 451 | + ? Filter::unlike('service_id', '*') |
| 452 | + : Filter::equal('service_id', $serviceId); |
| 453 | + // Use the latest event at or before the beginning of the SLA interval as the initial state. |
| 454 | + $hardState = SlaHistoryState::on($this->getDb()) |
| 455 | + ->columns(['hard_state']) |
| 456 | + ->filter( |
| 457 | + Filter::all( |
| 458 | + Filter::equal('host_id', $hostId), |
| 459 | + $serviceFilter, |
| 460 | + Filter::lessThanOrEqual('event_time', $start) |
| 461 | + ) |
| 462 | + ) |
| 463 | + ->resetOrderBy() |
| 464 | + ->orderBy('event_time', 'DESC') |
| 465 | + ->limit(1); |
| 466 | + |
| 467 | + $hardState = $hardState->first(); |
| 468 | + |
| 469 | + // If this doesn't exit, use the previous state from the first event after the beginning of the SLA interval. |
| 470 | + if (! $hardState) { |
| 471 | + $hardState = SlaHistoryState::on($this->getDb()) |
| 472 | + ->columns(['hard_state' => 'previous_hard_state']) |
| 473 | + ->filter( |
| 474 | + Filter::all( |
| 475 | + Filter::equal('host_id', $hostId), |
| 476 | + $serviceFilter, |
| 477 | + Filter::greaterThan('event_time', $start) |
| 478 | + ) |
| 479 | + ); |
| 480 | + |
| 481 | + $hardState = $hardState->first(); |
| 482 | + |
| 483 | + // If this also doesn't exist, use the current host/service state. |
| 484 | + if (! $hardState) { |
| 485 | + if ($serviceId !== null) { |
| 486 | + $hardState = ServiceState::on($this->getDb()) |
| 487 | + ->filter(Filter::equal('service_id', $serviceId)); |
| 488 | + } else { |
| 489 | + $hardState = HostState::on($this->getDb()); |
| 490 | + } |
| 491 | + |
| 492 | + $hardState |
| 493 | + ->columns(['hard_state']) |
| 494 | + ->filter(Filter::equal('host_id', $hostId)); |
| 495 | + |
| 496 | + $hardState = $hardState->first(); |
| 497 | + } |
| 498 | + } |
| 499 | + |
| 500 | + return $hardState === null ? $initialHardState : (int) $hardState->hard_state; |
| 501 | + } |
279 | 502 | } |
0 commit comments