diff options
Diffstat (limited to 'library/Icingadb/Widget/Detail/CheckStatistics.php')
-rw-r--r-- | library/Icingadb/Widget/Detail/CheckStatistics.php | 373 |
1 files changed, 373 insertions, 0 deletions
diff --git a/library/Icingadb/Widget/Detail/CheckStatistics.php b/library/Icingadb/Widget/Detail/CheckStatistics.php new file mode 100644 index 0000000..8a826d5 --- /dev/null +++ b/library/Icingadb/Widget/Detail/CheckStatistics.php @@ -0,0 +1,373 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Date\DateFormatter; +use Icinga\Module\Icingadb\Widget\CheckAttempt; +use Icinga\Util\Format; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\FormattedString; +use ipl\Html\HtmlElement; +use ipl\Html\HtmlString; +use ipl\Html\Text; +use ipl\Web\Common\Card; +use ipl\Web\Widget\EmptyState; +use ipl\Web\Compat\StyleWithNonce; +use ipl\Web\Widget\TimeAgo; +use ipl\Web\Widget\TimeSince; +use ipl\Web\Widget\TimeUntil; +use ipl\Web\Widget\VerticalKeyValue; + +class CheckStatistics extends Card +{ + const TOP_LEFT_BUBBLE_FLAG = <<<'SVG' +<svg viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'> + <path class='bg' d='M0 0L13 13L3.15334e-06 13L0 0Z'/> + <path class='border' fill-rule='evenodd' clip-rule='evenodd' + d='M0 0L3.3959e-06 14L14 14L0 0ZM1 2.41421L1 13L11.5858 13L1 2.41421Z'/> +</svg> +SVG; + + const TOP_RIGHT_BUBBLE_FLAG = <<<'SVG' +<svg viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'> + <path class='bg' d="M12 0L-1 13L12 13L12 0Z"/> + <path class='border' fill-rule="evenodd" clip-rule="evenodd" + d="M12 0L12 14L-2 14L12 0ZM11 2.41421L11 13L0.414213 13L11 2.41421Z"/> +</svg> +SVG; + + protected $object; + + protected $tag = 'div'; + + protected $defaultAttributes = ['class' => ['progress-bar', 'check-statistics']]; + + public function __construct($object) + { + $this->object = $object; + } + + protected function assembleBody(BaseHtmlElement $body) + { + $styleElement = (new StyleWithNonce()) + ->setModule('icingadb'); + + $hPadding = 10; + $durationScale = 80; + $checkInterval = $this->getCheckInterval(); + + $timeline = new HtmlElement('div', Attributes::create(['class' => ['check-timeline', 'timeline']])); + $above = new HtmlElement('ul', Attributes::create(['class' => 'above'])); + $below = new HtmlElement('ul', Attributes::create(['class' => 'below'])); + $progressBar = new HtmlElement('div', Attributes::create(['class' => 'bar'])); + $overdueBar = null; + + $now = time(); + $executionTime = ($this->object->state->execution_time / 1000) + ($this->object->state->latency / 1000); + + $nextCheckTime = $this->object->state->next_check !== null && ! $this->isChecksDisabled() + ? $this->object->state->next_check->getTimestamp() + : null; + if ($this->object->state->is_overdue) { + $nextCheckTime = $this->object->state->next_update->getTimestamp(); + + $durationScale = 60; + + $overdueBar = new HtmlElement( + 'div', + Attributes::create(['class' => 'timeline-overlay']), + new HtmlElement('div', Attributes::create(['class' => 'now'])) + ); + + $above->addHtml(new HtmlElement( + 'li', + Attributes::create(['class' => 'now']), + new HtmlElement( + 'div', + Attributes::create(['class' => 'bubble']), + new HtmlElement('strong', null, Text::create(t('Now'))) + ) + )); + + $this->getAttributes()->add('class', 'check-overdue'); + } else { + $progressBar->addHtml(new HtmlElement('div', Attributes::create(['class' => 'now']))); + } + + if ($nextCheckTime !== null && ! $this->object->state->is_overdue && $nextCheckTime < $now) { + // If the next check is already in the past but not overdue, it means the check is probably running. + // Icinga only updates the state once the check reports a result, that's why we have to simulate the + // execution start and end time, as well as the next check time. + $lastUpdateTime = $nextCheckTime; + $nextCheckTime = $this->object->state->next_update->getTimestamp() - $executionTime; + $executionEndTime = $lastUpdateTime + $executionTime; + } else { + $lastUpdateTime = $this->object->state->last_update !== null + ? $this->object->state->last_update->getTimestamp() - $executionTime + : null; + $executionEndTime = $this->object->state->last_update !== null + ? $this->object->state->last_update->getTimestamp() + : null; + } + + if ($this->object->state->is_overdue) { + $leftNow = 100; + } elseif ($nextCheckTime === null) { + $leftNow = 0; + } elseif (! $this->object->state->is_reachable && time() - $executionEndTime > $checkInterval * 2) { + // We have no way of knowing whether the dependency pauses check scheduling. + // The only way to detect this, is to measure how old the last update is. + $nextCheckTime = null; + $leftNow = 0; + } elseif ($nextCheckTime - $lastUpdateTime <= 0) { + $leftNow = 0; + } else { + $leftNow = 100 * (1 - ($nextCheckTime - time()) / ($nextCheckTime - $lastUpdateTime)); + if ($leftNow > 100) { + $leftNow = 100; + } elseif ($leftNow < 0) { + $leftNow = 0; + } + } + + $styleElement->addFor($progressBar, ['width' => sprintf('%F%%', $leftNow)]); + + $leftExecutionEnd = $nextCheckTime !== null && $nextCheckTime - $lastUpdateTime > 0 ? $durationScale * ( + 1 - ($nextCheckTime - $executionEndTime) / ($nextCheckTime - $lastUpdateTime) + ) : 0; + + $markerLast = new HtmlElement('div', Attributes::create([ + 'class' => ['highlighted', 'marker', 'left'], + 'title' => $lastUpdateTime !== null ? DateFormatter::formatDateTime($lastUpdateTime) : null + ])); + $markerNext = new HtmlElement('div', Attributes::create([ + 'class' => ['highlighted', 'marker', 'right'], + 'title' => $nextCheckTime !== null ? DateFormatter::formatDateTime($nextCheckTime) : null + ])); + $markerExecutionEnd = new HtmlElement('div', Attributes::create(['class' => ['highlighted', 'marker']])); + $styleElement->addFor($markerExecutionEnd, [ + 'left' => sprintf('%F%%', $hPadding + $leftExecutionEnd) + ]); + + $progress = new HtmlElement('div', Attributes::create([ + 'class' => ['progress', time() < $executionEndTime ? 'running' : null] + ]), $progressBar); + if ($nextCheckTime !== null) { + $progress->addAttributes([ + 'data-animate-progress' => true, + 'data-start-time' => $lastUpdateTime, + 'data-end-time' => $nextCheckTime, + 'data-switch-after' => $executionTime, + 'data-switch-class' => 'running' + ]); + } + + $timeline->addHtml( + $progress, + $markerLast, + $markerExecutionEnd, + $markerNext + )->add($overdueBar); + + $executionStart = new HtmlElement( + 'li', + Attributes::create(['class' => 'left']), + new HtmlElement( + 'div', + Attributes::create(['class' => ['bubble', 'upwards', 'top-right-aligned']]), + new VerticalKeyValue( + t('Execution Start'), + $lastUpdateTime ? new TimeAgo($lastUpdateTime) : t('PENDING') + ), + HtmlString::create(self::TOP_RIGHT_BUBBLE_FLAG) + ) + ); + $executionEnd = new HtmlElement( + 'li', + Attributes::create(['class' => 'positioned']), + new HtmlElement( + 'div', + Attributes::create(['class' => ['bubble', 'upwards', 'top-left-aligned']]), + new VerticalKeyValue( + t('Execution End'), + $executionEndTime !== null + ? ($executionEndTime > $now + ? new TimeUntil($executionEndTime) + : new TimeAgo($executionEndTime)) + : t('PENDING') + ), + HtmlString::create(self::TOP_LEFT_BUBBLE_FLAG) + ) + ); + + $styleElement->addFor($executionEnd, ['left' => sprintf('%F%%', $hPadding + $leftExecutionEnd)]); + + $intervalLine = new HtmlElement( + 'li', + Attributes::create(['class' => 'interval-line']), + new VerticalKeyValue(t('Interval'), Format::seconds($checkInterval)) + ); + + $styleElement->addFor($intervalLine, [ + 'left' => sprintf('%F%%', $hPadding + $leftExecutionEnd), + 'width' => sprintf('%F%%', $durationScale - $leftExecutionEnd) + ]); + + $executionLine = new HtmlElement( + 'li', + Attributes::create(['class' => ['interval-line', 'execution-line']]), + new VerticalKeyValue( + sprintf('%s / %s', t('Execution Time'), t('Latency')), + FormattedString::create( + '%s / %s', + $this->object->state->execution_time !== null + ? Format::seconds($this->object->state->execution_time / 1000) + : (new EmptyState(t('n. a.')))->setTag('span'), + $this->object->state->latency !== null + ? Format::seconds($this->object->state->latency / 1000) + : (new EmptyState(t('n. a.')))->setTag('span') + ) + ) + ); + + $styleElement->addFor($executionLine, [ + 'left' => sprintf('%F%%', $hPadding), + 'width' => sprintf('%F%%', $leftExecutionEnd) + ]); + + if ($executionEndTime !== null) { + $executionLine->addHtml(new HtmlElement('div', Attributes::create(['class' => 'start']))); + $executionLine->addHtml(new HtmlElement('div', Attributes::create(['class' => 'end']))); + } + + if ($this->isChecksDisabled()) { + $nextCheckBubbleContent = new VerticalKeyValue( + t('Next Check'), + t('n.a') + ); + + $this->addAttributes(['class' => 'checks-disabled']); + } else { + $nextCheckBubbleContent = $this->object->state->is_overdue + ? new VerticalKeyValue(t('Overdue'), new TimeSince($nextCheckTime)) + : new VerticalKeyValue( + t('Next Check'), + $nextCheckTime !== null + ? ($nextCheckTime > $now + ? new TimeUntil($nextCheckTime) + : new TimeAgo($nextCheckTime)) + : t('PENDING') + ); + } + + $nextCheck = new HtmlElement( + 'li', + Attributes::create(['class' => 'right']), + new HtmlElement( + 'div', + Attributes::create(['class' => ['bubble', 'upwards']]), + $nextCheckBubbleContent + ) + ); + + $above->addHtml($executionLine); + + $below->addHtml( + $executionStart, + $executionEnd, + $intervalLine, + $nextCheck + ); + + $body->addHtml($above, $timeline, $below, $styleElement); + } + + /** + * Checks if both active and passive checks are disabled + * + * @return bool + */ + protected function isChecksDisabled(): bool + { + return ! ($this->object->active_checks_enabled || $this->object->passive_checks_enabled); + } + + protected function assembleHeader(BaseHtmlElement $header) + { + $checkSource = (new EmptyState(t('n. a.')))->setTag('span'); + if ($this->object->state->check_source) { + $checkSource = Text::create($this->object->state->check_source); + } + + $header->addHtml( + new VerticalKeyValue(t('Command'), $this->object->checkcommand_name), + new VerticalKeyValue( + t('Scheduling Source'), + $this->object->state->scheduling_source ?? (new EmptyState(t('n. a.')))->setTag('span') + ) + ); + + if ($this->object->timeperiod->id) { + $header->addHtml(new VerticalKeyValue( + t('Timeperiod'), + $this->object->timeperiod->display_name ?? $this->object->timeperiod->name + )); + } + + $header->addHtml( + new VerticalKeyValue( + t('Attempts'), + new CheckAttempt((int) $this->object->state->check_attempt, (int) $this->object->max_check_attempts) + ), + new VerticalKeyValue(t('Check Source'), $checkSource) + ); + } + + /** + * Get the active `check_interval` OR `check_retry_interval` + * + * @return int + */ + protected function getCheckInterval(): int + { + if (! ($this->object->state->is_problem && $this->object->state->state_type === 'soft')) { + return $this->object->check_interval; + } + + $delay = ($this->object->state->execution_time + $this->object->state->latency) / 1000 + 5; + $interval = $this->object->state->next_check->getTimestamp() + - $this->object->state->last_update->getTimestamp(); + + // In case passive check is used, the check_retry_interval has no effect. + // Since there is no flag in the database to check if the passive check was triggered. + // We have to manually check if the check_retry_interval matches the calculated interval. + if ( + $this->object->check_retry_interval - $delay <= $interval + && $this->object->check_retry_interval + $delay >= $interval + ) { + return $this->object->check_retry_interval; + } + + return $this->object->check_interval; + } + + protected function assemble() + { + parent::assemble(); + + if ($this->isChecksDisabled()) { + $this->addHtml(new HtmlElement( + 'div', + Attributes::create(['class' => 'checks-disabled-overlay']), + new HtmlElement( + 'strong', + Attributes::create(['class' => 'notes']), + Text::create(t('active and passive checks are disabled')) + ) + )); + } + } +} |