SVG; const TOP_RIGHT_BUBBLE_FLAG = <<<'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')) ) )); } } }