summaryrefslogtreecommitdiffstats
path: root/library/Icingadb/Widget/Detail/CheckStatistics.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icingadb/Widget/Detail/CheckStatistics.php')
-rw-r--r--library/Icingadb/Widget/Detail/CheckStatistics.php373
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'))
+ )
+ ));
+ }
+ }
+}